新機能がもたらす、画面遷移実装の進化
データ駆動の新しいNavigationAPI、NavigationStack
まず最初は、SwiftUIでの画面遷移の実装に関する大きなアップデートです。
従来のSwiftUIでは、ナビゲーションバーを伴うドリルダウン型の画面遷移を実現するためにはNavigationViewというコンポーネントを利用していました。今回のアップデートではそれが非推奨になり、最新のSwiftUIではNavigationStackというコンポーネントの利用が推奨されています。
まずは従来のNavigationViewを用いた実装方法から見てみましょう。コードとしては下記のようになります。
struct FirstView: View { var body: some View { NavigationView { List(0..<10) { number in NavigationLink { SecondView(number: number) } label: { Text("\(number)") } }.navigationTitle("FirstView") } } }
このようにして、NavigationViewを用いた画面遷移の実装は、下記のようなステップで行う形になります。
- NavigationView配下にNavigationLinkを設置する
- NavigationLinkで遷移を発火させるビューと遷移対象画面を紐づける
これをNavigationStackを用いて同じ挙動を実現する場合、下記のステップで実装を行う形に変わります。
- NavigationStack配下にNavigationLinkを設置する
- NavigationLinkで遷移を発火させるビューを定義し、イニシャライザで遷移対象画面を一意に紐づけるための値を受け渡す
- navigationDestinationModifierを宣言して、2で受け渡された値ごとの遷移先Viewを決定する
具体的なコードとしては下記の通りになります。
struct FirstView: View { var body: some View { // NavigationViewの代わりにNavigationStackを用いる NavigationStack { List(0..<10) { number in // NavigationLinkの引数には遷移に関連する値を受け渡す NavigationLink(value: number) { Text("\(number)") } } .navigationTitle(Text("First")) // NavigationLinkに受け渡される可能性のある型ごとにnavigationDestinationModifierを設定する .navigationDestination(for: Int.self) { value in // 値に対応したViewインスタンスを生成して返す SecondView(number: value) } } } }
実装ステップが増えているため、一見すると複雑性が増していると思うかもしれません。しかし、NavigationStackは要件が増えた場合にも一貫してシンプルな状態を保てるという特徴があります。またNavigationViewでは細かい遷移の挙動を実現しようとした場合に、途端に実装コードが複雑になってしまう課題がありましたが、NavigationStackを使えばその影響を最小限に保つことができます。
具体的な例を見ていきます。先ほどのサンプルコードに対して、画面遷移をコントロールしやすくなるような改修を加えてみましょう。現状、NavigationLinkにはIntの値を受け渡していますが、遷移先が明確になるよう専用の型を定義することで保守性を向上させることも可能です。
// 特定のルート画面からどういった画面遷移が発生するかを列挙するための型 enum NavigationDestination: Hashable { case second(Int) case third } struct FirstView: View { var body: some View { NavigationStack { List(0..<10) { number in // NavigationLinkとNavigationDestinationを対応つける NavigationLink(value: NavigationDestination.second(number)) { Text("\(number)") } } .navigationTitle(Text("First")) .navigationDestination(for: NavigationDestination.self) { destination in // 子View内で遷移が発火したNavigationDestinationごとの遷移先をswitchで網羅 switch destination { case .second(let number): SecondView(number: number) case .third: ThirdView() } } } } } struct SecondView: View { let number: Int var body: some View { List(0..<5) { number in // NavigationLinkとNavigationPathを対応つける NavigationLink(value: NavigationDestination.third) { Text("\(number)") } }.navigationTitle(Text("\(number)")) } } struct ThirdView: View { var body: some View { Text("") .navigationTitle(Text("Third")) } }
さらに、NavigationStackには「遷移のコントロール」に関してもより細かく制御できる仕組みが備わっています。例えば、NavigationViewでは実現が大変だったシステム制御による画面のpush/pop処理がとても簡単になっています。実際に実装コードを見てみましょう。
まず、NavigationStackを作成する際に遷移先の情報を保持するStateインスタンスを受け渡しておきます。そしてそのStateを任意のタイミングで制御することで細かな遷移のコントロールが実現可能になります。
struct FirstView: View { // 遷移先のデータを保持するためのStateを用意 @State private var navigationPath: [NavigationDestination] = [] var body: some View { // NavigationStackのイニシャライザで作成したStateを受け渡しておく NavigationStack(path: $navigationPath) { List(0..<10) { number in Button("\(number)") { // NavigationLinkを使わずにnavigationPathの配列を制御することで遷移を発火させる navigationPath.append(.second(number)) } } .navigationTitle(Text("First")) .navigationDestination(for: NavigationDestination.self) { destination in switch destination { case .second(let number): SecondView(number: number) case .third: // 子Viewにpathのデータを受け渡しておく ThirdView(navigationPath: $navigationPath) } } } } } struct ThirdView: View { @Binding var navigationPath: [NavigationDestination] var body: some View { Button("トップに戻る") { // 配列から遷移対象となる画面以降のインスタンスを除去することで、対象画面まで戻ることが可能 navigationPath.removeAll() }.navigationTitle(Text("Third")) } }
このように、navigationPathの配列を制御することで、システム的な画面遷移の発火だけでなく、特定の画面に直接飛ぶことも簡単に実現できるようになりました。NavigationStackを用いて「データと遷移先を紐づける」ことにより、より柔軟な画面制御をシンプルな記述で表現できるようになりました。
NavigationSplitViewによるデバイス最適されたマルチカラムレイアウトの実現
NavigationSplitViewはiOS 16で追加された新しいコンテナ型のコンポーネントです。
このViewはメーラーなど複数カラムのUIを実現したい場合に特に効果を発揮します。例えば、iOSでは単一列表示にし、iPadでは複数列表示にするなど、デバイスに応じた最適なレイアウト選択をOS側で自動的に行ってくれます。NavigationStackと一緒に動作させることも可能なので、複雑なナビゲーション構造を構築することにも役立つでしょう。
Half-Modalの表示
iOS 16のSwiftUIではハーフモーダルの表示も簡単です。iOS標準のハーフモーダル表示は、iOS 15時点ではUIKitでのみ提供されていました。これがSwiftUIでも利用可能になった形となります。まずは通常のモーダル表示の実装から見てみましょう。
struct ModalView: View { @State private var showSheet = false var body: some View { Button("Show") { showSheet = true }.sheet(isPresented: $showSheet) { Text("Hello") } } }
こちらのコードを実行すると下記のような形でViewを表示することができます。
この状態から表示をハーフモーダルに切り替えてみましょう。必要な作業は、presentationDetentsModifierを用いたモーダルのサイズ指定を追加するだけです。
struct HalfModalView: View { @State private var showSheet = false var body: some View { Button("Show") { showSheet = true }.sheet(isPresented: $showSheet) { Text("Hello") // 追加 .presentationDetents([.height(250), .medium]) } } }
引数に複数の値を受け渡すことでリサイズを可能にすることもできるので、用途に応じて使い分けると良いでしょう。