Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

【iPadOS 18】Tab barのUI / UXの大幅アップデート

はじめに

みなさん、こんにちは!ヤプリでiOSエンジニアをしております、白数 (@cychow_app) です。
本記事は「ヤプリ Advent Calendar 2024」のDay 17となります!
本記事では、iPadOS 18から大きく変更されたTab barに関して、以下の内容に焦点を当てて解説していきます。

  • WWDC24で発表されたTab barはどのようにUI / UX変更されたのか
  • 新たに登場したTab bar API

また、新たに追加されたAPIを用いて、どのようにTab barを構築していくのかを実際のコードを交えて解説してきます。

iPadOS 18のTab barについて

WWDC24にてTab barの更新が発表

1年に一度、Apple Platformに関して更新内容が発表されるイベント「Worldwide Developers Conference 2024 (WWDC24) 」が2024年6月11日 〜 2024年6月15日 (JST) の期間で開催されました。更新内容はApple Developerサイトでも確認でき、その中でも「Elevate your tab and sidebar experience in iPadOS」というセッションの中で、iPadOS 18におけるTab barに対して変更が加えられているか、説明されていました。 具体的な変更点は以下となります。

  • Tab barが画面下部表示から画面上部表示に
  • Tab barの内容をSidebar内で表示可能に
  • Sidebarをカスタマイズし、アプリ内のセクションへのアクセスを制御することが可能に

Human Interface Guidelineでは…

iPadOS 18からTab barが更新されたことにより、Human Interface Guidelineも更新されました。 iPadOS 18からの新しいTab barのデザインやTab barから切り替え表示が可能なSidebarのデザインが紹介されています。

Tab bar

Sidebar

また、Tab barが画面上部に移動し、かつTab barの内容をSide Barとして表示可能となったことで、ユーザーがより幅広いアプリのセクションにアクセスしやすくなったという内容が記載されています。 詳しくは以下で紹介されていますので、ぜひ確認してみてください!

developer.apple.com

新たなTab barの構築方法

こちらでは上記で紹介した、WWDC24のセッション「Elevate your tab and sidebar experience in iPadOS」内で実際に紹介されている新たなTab bar APIを使用して、Tab barを構築していきたいと思います。

SwiftUIによるTab barの構築

iPadOS 17 (iOS 17) 以前でのTab barの構築方法は、TabViewクロージャ内部で、Tab barに表示したい画面のビューを直接呼び出し、そのビューに対してtabItem(_:)メソッドを使用することで、タブにテキストやイメージを追加します。

TabView {
    FirstView()
        .tabItem {
            Image(systemName: "1.circle")
            Text("First View")
        }
    SecondView()
        .tabItem {
            Image(systemName: "2.circle")
            Text("Second View")
        }
    // ...
}

iPadOS 18 (iOS 18) 以降ではtabItem(_:)メソッドが非推奨 (deprecated)となり、TabViewが改善され、Tab構造体を用いて構築していく方法になりました。
(※ Tab構造体はiOS17以前に対してバックデプロイされていない模様 (2024/12/16時点))

TabView {
    Tab("First View", systemImage: "1.circle") {
        FirstView()
    }
    Tab("Second View", systemImage: "2.circle") {
        SecondView()
    }
    // ...
}

また、新たにTabRoleという構造体も追加されており、Tab構造体の引数roleに.searchを指定することで、アプリ共通機能である「検索機能」をTab barの固定の位置にタブを配置することができます。

TabView {
    // ...
    Tab(role: .search) {
        SearchView()
    }
}

続いて、Tab barとSidebarの表示切り替え方法についてです。iPadOS 18からTab barの内容をSidebarに表示する機能が追加されました。Sidebarの機能を利用するためにはまず、TabViewに対してtabViewStyle(_:)メソッドで「.sidebarAdaptable」を指定します。これによってTab barにSidebarのメニューが追加されます。

TabView {
    // ...
}
.tabViewStyle(.sidebarAdaptable)

これにより、以下のようなSidebarを表示することができ、Tab barの内容を階層構造状に表示することができます。

続いて、Sidebar内で構築可能なグループ機能についてです。階層構造状に表示することで、すばやくユーザーが求めるコンテンツにアクセスすることができ、タブコンテンツの全体を表示することができます。タブコンテンツを効率的に表示するための機能の一つとしてグループ機能が、iPadOS 18以降から新たに追加されました。グループ機能を利用するためには、TabViewクロージャ内でTabSection構造体を呼び出します。これによって、TabSectionクロージャ内で内部で呼び出しいてるTab構造体をグルーピングして、Sidebar内で表示することができます。

TabView() {
    // ...
    TabSection("Collection") {
        Tab("Third View", systemImage: "3.circle") {
            SecondView()
        }
        Tab("Forth View", systemImage: "4.circle") {
            SecondView()
        }
    }
    // ...
}

また、よく使う機能に迅速にアクセスしたい場合はsectionActions(content:)メソッドを使用し、グループにアクションを追加することができます。(今回は新しいタブを作成するための機能をアクションに追加しています。)

TabView() {
    TabSection("Collection") {
        // ...
    }
    .sectionActions {
        Button("New Tab", systemImage: "plus.square") {
            // add new tab function...
        }
    }
}

グループ機能以外にも、ドラッグ&ドロップを可能とするdropDestination(for:action:)メソッドや、Tab barをより利便性が良いものにするために詳細にカスタマイズを行うことを可能とする、TabViewCustomization構造体などが登場しています。Appleからも新たなTab bar APIを使用した公式チュートリアルがアップロードされていますので、併せてご確認ください。

developer.apple.com

UIKitによるTab barの構築

SwiftUIでの構築方法に続き、UIKitでの構築方法も見ていきます。
iPadOS 17 (iOS 17) 以前でのTab barの構築方法は、UITabBarControllerのviewControllersプロパティに、タブに追加したいViewControllerを配列として渡してあげます。

class TabBarController: UITabBarController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let firstViewStoryboard = UIStoryboard(name: "FirstViewController", bundle: nil)
        let firstViewController = firstViewStoryboard.instantiateViewController(withIdentifier: "FirstViewController") as! FirstViewController
        firstViewController.tabBarItem = UITabBarItem(title: "First View", image: UIImage(systemName: "1.circle"), selectedImage: nil)
        
        let secondViewStoryboard = UIStoryboard(name: "SecondViewController", bundle: nil)
        let secondViewController = secondViewStoryboard.instantiateViewController(withIdentifier: "SecondViewController") as! SecondViewController
        secondViewController.tabBarItem = UITabBarItem(title: "Second View", image: UIImage(systemName: "2.circle"), selectedImage: nil)
        
        viewControllers = [firstViewController, secondViewController]
    }
}

iPadOS 18 (iOS 18) 以降では、新たに追加された、UITabBarControllerのtabsプロパティに、同様に新たに登場したUITabクラスを配列として渡してあげることでTab barを構築することができます。

class TabBarController: UITabBarController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        tabs = [
            UITab(title: "First View", image: UIImage(systemName: "1.circle"), identifier: "Tabs.firstView") { _ in
                let storyboard = UIStoryboard(name: "FirstViewController", bundle: nil)
                return storyboard.instantiateViewController(withIdentifier: "FirstViewController") as! FirstViewController
            },
            UITab(title: "Second View", image: UIImage(systemName: "2.circle"), identifier: "Tabs.secondView") { _ in
                let storyboard = UIStoryboard(name: "SecondViewController", bundle: nil)
                return storyboard.instantiateViewController(withIdentifier: "SecondViewController") as! SecondViewController
            }
        ]
    }
}

元々使用していたviewControllersプロパティはiPadOS 18では非推奨 (deprecated) とはなっていないものの、tabsプロパティの定義箇所に以下のような文章が記載されています。

/// An array of root tabs representing view controllers to display by the tab bar interface. Default is empty.
/// Once set, `UITabBarController.viewControllers` and related properties and methods will not be called.
@available(iOS 18.0, *)
open var tabs: [UITab]

タブバーインターフェイスで表示するビューコントローラを表すルートタブの配列。デフォルトは空です。 一度設定すると、UITabBarController.viewControllers や関連するプロパティやメソッドは呼び出されなくなる。(日本語訳)

上記の内容から、viewControllersプロパティをはじめとする既存で使用しているUITabBarControllerの関連プロパティやメソッドが非推奨 (deprecated) とならないかは注視しておく必要があるかと思います。

また、新たにUISearchTabというクラスも追加されており、tabsプロパティの配列内に追加することで、アプリ共通機能である「検索機能」をTab barの固定の位置にタブを配置することができます。

class TabBarController: UITabBarController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        tabs = [
            // ...
            UISearchTab { _ in
                let storyboard = UIStoryboard(name: "SearchViewController", bundle: nil)
                return storyboard.instantiateViewController(withIdentifier: "SearchViewController") as! SearchViewController
            }
        ]
    }
}

続いて、UIKitでのTab barとSidebarの表示切り替え方法についてです。Sidebarの機能を利用するためには、新たに追加されたUITabBarControllerのmodeプロパティに「.tabSidebar」を指定します。これによってTab barにSidebarのメニューが追加されます。

class TabBarController: UITabBarController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        mode = .tabSidebar
        tabs = [
            // ...
        ]
    }
}

これにより、SwiftUIの時と同様に、Sidebarを表示することができ、Tab barの内容を階層構造状に表示することができます。ただし、UIKitではデフォルトでEditボタンが付与されているみたいです。

続いて、Sidebar内のグループ機能についてです。グループ機能を利用するためには、UITabGroupクラスを利用します。UITabGroupを初期化し、tabsプロパティの配列に追加することでグルーピングされたタブコンテンツをSidebarに表示することができます。

class TabBarController: UITabBarController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // ...
        
        let collectionsGroup = UITabGroup(
            title: "Collections",
            image: UIImage(systemName: "folder"),
            identifier: "Tabs.CollectionsGroup",
            children: collectionsTabs()) { _ in
                let storyboard = UIStoryboard(name: "ThirdViewController", bundle: nil)
                return storyboard.instantiateViewController(withIdentifier: "ThirdViewController") as! ThirdViewController
        }
        
        tabs = [
            // ...
            collectionsGroup,
            // ...
        ]
    }
    
    private func collectionsTabs() -> [UITab] {
        [
            UITab(title: "Third View", image: UIImage(systemName: "3.circle"), identifier: "Tabs.thirdView") { _ in
                let storyboard = UIStoryboard(name: "ThirdViewController", bundle: nil)
                return storyboard.instantiateViewController(withIdentifier: "FirstViewController") as! FirstViewController
            },
            UITab(title: "Forth View", image: UIImage(systemName: "4.circle"), identifier: "Tabs.forthView") { _ in
                let storyboard = UIStoryboard(name: "ForthViewController", bundle: nil)
                return storyboard.instantiateViewController(withIdentifier: "ForthViewController") as! ForthViewController
            }
        ]
    }
}

また、よく使う機能に迅速にアクセスしたい場合は、UITabGroupのsidebarActionsプロパティにUIActionを登録することで、グループにアクションを追加することができます。(今回は新しいタブを作成するための機能をアクションに追加しています。)

class TabBarController: UITabBarController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let collectionsGroup = UITabGroup(
            // ...
        }
        
        collectionsGroup.sidebarActions = [
            UIAction(title: "New Tab", image: UIImage(systemName: "plus.square")) { _ in
                // add new tab function...
            }
        ]
        
        // ...
    }
    // ...
}

SwiftUIでの構築の時と同様に、ドラッグ&ドロップや、Tab barをより利便性が良いものにするために詳細にカスタマイズを行うことを可能とする、APIなどが登場しています。
AppleからもUIKitのTab bar APIを使用するための公式ドキュメントがアップロードされていますので、併せてご確認ください。

developer.apple.com

最後に

今回は、iPadOS 18から大きく変更が入ったTab barに焦点を当て、「Tab barはどのようにUI/UX変更されたのか」、「新たなTab barはどのように構築するのか」などについてご紹介しました。 今回実装した内容は以下のGitHubのリポジトリとしてアップロードしておりますので、参考にしていただけますと幸いです。

github.com

github.com

また、ヤプリでは現在エンジニア採用に力を入れております。
本記事についてや、ヤプリについて具体的なお話を聞いてみたいと思っていただいていた場合は、ぜひカジュアル面談にご応募いただければと思います!

open.talentio.com

それでは、ここまでお読みいただきありがとうございました!!
そして少し早いですが、🎄Happy Holidays!!!🎄