これははてなエンジニアアドベントカレンダー2022 37日目の記事です。
2回目の登場 id:yutailang0119 です、おはこんハロチャオ~!
昨日は id:hogashi の aタグで#topにリンクするとページ先頭にスクロールするのは仕様 - hogashi.* でした。
冬休みはあつ森で年越ししながら、SwiftUIの2022年OS対応をしていた
待機 #どうぶつの森 #AnimalCrossing #ACNH #NintendoSwitch pic.twitter.com/d9m1vyYzdt
— Yutaro Muta (@yutailang0119) 2022年12月31日
Migrating to new navigation types や NavigationStack
を読んで、"programmatic navigation" のパターンをイメージしづらかったので、サンプルを用意しながら動きを見てみた備考録。
Enviromnent
- Apple Swift version 5.7.2 (swiftlang-5.7.2.135.5 clang-1400.0.29.51)
- Xcode Version 14.2 (14C18)
- iOS 16.2
単一の型に対応した画面遷移
まずはPark
型を単独で扱う画面遷移パターン。
NavigationStack
に記載されるコード断片の裏側を推測して進めていく。
対応する型 -- Park
SwiftUIでナビゲーションに対応する型には Hashable
にconformさせる。
Identifiable
にもするのは、List
で扱いやすくするため。
struct Park: Identifiable, Hashable { var id: String { name } var name: String }
画面遷移の実装
画面遷移にはNavigationStack
のiOS 16新API init(_:value:)
とnavigationDestination(for:destination:)
を使う。
import SwiftUI struct ContentView: View { var parks: [Park] = [ Park(name: "Yosemite"), Park(name: "Sequoia"), ] var body: some View { NavigationStack { List(parks) { park in NavigationLink(park.name, value: park) } } .navigationDestination(for: Park.self) { park in ParkDetails(park: park) } .navigationTitle(String(describing: type(of: self))) } } struct ParkDetails: View { var park: Park var body: some View { Text(park.name) .navigationTitle(String(describing: type(of: self))) } }
これで Park
のリストをタップすると ParkDetails
の画面に遷移する。
複数の型に対応した画面遷移
ここから、上記の例を拡張して、複数の型を扱うことを想定してみる。
Location
を同一画面に表示して、LocatonDetails
画面に遷移できるようにする。
struct ContentView: View { var parks: [Park] = [ Park(name: "Yosemite"), Park(name: "Sequoia"), ] var locations: [Location] = [ Location(name: "Cupertino"), Location(name: "San Jose"), ] var body: some View { NavigationStack { List { Section { ForEach(parks) { park in NavigationLink(park.name, value: park) } } Section { ForEach(locations) { location in NavigationLink(location.name, value: location) } } } .navigationDestination(for: Park.self) { park in ParkDetails(park: park) } .navigationDestination(for: Location.self) { location in Text(location.name) } .navigationTitle(String(describing: type(of: self))) } } }
navigationDestination(for:destination:)
は、メソッドチェーンで複数連結し、異なる型の遷移先を定義できる。
NavigationStack
のスタックを編集
NavigationStack
の "Manage navigation state" にあるように、init(path:root:)
を使うと画面スタックをprogrammaticに操作できる。
例えば、一度にスタックを2つ重ねて遷移させることで、遷移元と遷移先画面の間に画面を挟んでスタックできる。
データの配列をBindして、ナビゲーション操作
struct ContentView: View { (省略) @State private var presentedParks: [Park] = [] var body: some View { NavigationStack(path: $presentedParks) { List {(省略)} .navigationDestination(for: Park.self) { park in ParkDetails(park: park) } .navigationDestination(for: Location.self) { location in LocationDetails(location: location) } .navigationTitle(String(describing: type(of: self))) .toolbar { ToolbarItem { Button { presentedParks = [ Park(name: "Yosemite"), Park(name: "Sequoia"), ] DispatchQueue.main.asyncAfter(deadline: .now() + 1) { presentedParks.removeAll() } } label: { Text("Yosemite+Sequoia") } } } } } }
この場合、"Yosemite+Sequoia"ボタンを押すと ParkDetails
が2つ重なるスタックで画面遷移した後、全画面 dismiss
して元の画面に戻った遷移になる。
presentedParks
の配列を操作することで、画面スタックをコントロールすることができるということだ。
しかし、残念なことに LocationDetails
への遷移は動かなくなってしまう。
NavigationPath
をBindして、ナビゲーションを操作
ここで登場するのが、NavigationPath
。
A type-erased list of data representing the content of a navigation stack.
とある通り、型消去が使われている。
NavigationPath
を NavigationStack.init(path:root:)
にバインドして、ナビゲーションを操作する。
struct ContentView: View { (省略) @State private var path: NavigationPath = NavigationPath() var body: some View { NavigationStack(path: $path) { List {(省略)} .navigationDestination(for: Park.self) { park in ParkDetails(park: park) } .navigationDestination(for: Location.self) { location in LocationDetails(location: location) } .navigationTitle(String(describing: type(of: self))) .toolbar { ToolbarItem { Button { path.append(Park(name: "Yosemite")) path.append(Location(name: "Cupertino")) path.append(Park(name: "Sequoia")) } label: { Text("Yosemite+Cupertino+Sequoia") } } } } } }
これで ParkDetails
と LocationDetails
の両方に遷移可能。
この場合、"Yosemite+Cupertino+Sequoia"ボタンを押すと ParkDetails
> LocationDetails
> ParkDetails
の順にスタックで画面遷移をした状態になる。
データ型のBind vs NavigationPath
一見すると NavigationPath
で実装しておく方が、後々の拡張性が高そうに見える。
しかし、NavigationPath
にも弱点はある。
前述の通り、NavigationPath
は型消去のため、詳細なデータに直接アクセスすることができない。
また、配列操作と違い、任意のindexにアクセスできないため、細やかなスタック操作の面では NavigationPath
の分が悪そう。
要件に応じて、使い分けることになる。
まとめ
NavigationStack
を用いて、programmaticに画面遷移を操作する方法を、備考録としてまとめた。
path
をencode/decodeすることで、画面のState Restorationも可能になる設計になっているので、非常にかしこい。
iOS 16のことをまとめて知りたいあなたへ
年末年始休暇が終わってしまって時間が足りない方に朗報。
WEB+DB PRESS Vol.132 特集2「iOS 16最前線」には、iOS 16についてのエッセンスが詰まっています!
WEB+DB PRESS Vol.132 特集2 「iOS 16最前線」に寄稿しました #wdpress - がんばってなんか書く