SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

iOSエンジニアたちと振り返るWWDC

【WWDC2022総復習】「機能不足」は過去の話、さらに表現力を増したSwiftUI

  • X ポスト
  • このエントリーをはてなブックマークに追加

新機能がもたらす、画面遷移実装の進化

データ駆動の新しい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を用いた画面遷移の実装は、下記のようなステップで行う形になります。

  1. NavigationView配下にNavigationLinkを設置する
  2. NavigationLinkで遷移を発火させるビューと遷移対象画面を紐づける

 これをNavigationStackを用いて同じ挙動を実現する場合、下記のステップで実装を行う形に変わります。

  1. NavigationStack配下にNavigationLinkを設置する
  2. NavigationLinkで遷移を発火させるビューを定義し、イニシャライザで遷移対象画面を一意に紐づけるための値を受け渡す
  3. 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])
       }
   }
}

 引数に複数の値を受け渡すことでリサイズを可能にすることもできるので、用途に応じて使い分けると良いでしょう。

次のページ
SwiftUI単体で共有体験の実装が可能に

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
iOSエンジニアたちと振り返るWWDC連載記事一覧

もっと読む

この記事の著者

横山 拓也(STORES 株式会社)(ヨコヤマ タクヤ)

 STORES 株式会社テクノロジー部門モバイル本部にて、STORES レジ のiOSアプリ開発に従事。 水産学部出身でエンジニアになり、主にモバイルアプリ開発を主軸として経験を積み、現在ではAndroidやFlutterなども書く。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/17313 2023/03/10 11:00

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング