これははてなエンジニアアドベントカレンダー2022 37日目の記事です。
2回目の登場
id:yutailang0119 です、おはこんハロチャオ~!
昨日は
id:hogashi の aタグで#topにリンクするとページ先頭にスクロールするのは仕様 - hogashi.* でした。
冬休みはあつ森で年越ししながら、SwiftUIの2022年OS対応をしていた
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 - がんばってなんか書く