まえがき
WWDC2022にてNavigationStackが新しく発表されました。
iOS16からNavigationViewはdeprecatedで、NavigationStackを使わないといけないです。
SwiftUIは画面遷移周りがよくない・・・・!と、
SwiftUIを実戦投入していても、画面遷移まわりはUIKitを使用しているという話をチラホラききますし、
私が参画している現場においても画面遷移はUIKitで書いてます。
NavigationStackをどんなものか気になっていたので、軽く調査してみました。
Case 1, NavigationStackとNavigationLinkのみを使って遷移
NavigationStackの中にNavigationLinkを配置して遷移するだけ。
NavigationStackをdeprecatedされたNavigationViewのようです。
struct NavigationStackSample1: View {
let parkList: [Park] = [.init(name: "メリケンパーク"), .init(name: "万博記念公園"), .init(name: "奈良公園"), .init(name: "代々木公園")]
var body: some View {
NavigationStack {
List(parkList) { park in
NavigationLink(park.name) {
ParkDetailView(park: park)
}
}
.navigationTitle("公園リスト")
}
}
}
Case 2, NavigationStackとNavigationLinkとnavigationDestinationを使った例
struct NavigationStackSample2: View {
let parkList: [Park] = [.init(name: "メリケンパーク"), .init(name: "万博記念公園"), .init(name: "奈良公園"), .init(name: "代々木公園")]
let fruitsList: [Fruits] = [.init(name: "スイカ"), .init(name: "みかん"), .init(name: "りんご"), .init(name: "バナナ")]
var body: some View {
NavigationStack {
List(parkList) { park in
NavigationLink(park.name, value: park)
}
.navigationTitle("Park List")
.navigationDestination(for: Park.self) { park in
ParkDetailView(park: park)
}
.navigationDestination(for: Fruits.self) { fruits in
FruitsDetailView(fruits: fruits)
}
}
}
}
func navigationDestination<D, C>(
for data: D.Type,
destination: @escaping (D) -> C
) -> some View where D : Hashable, C : View
このnavigationDestinationはD.Typeを引数にとります。
NavigationLink(park.name, value: park)
.navigationDestination(for: Park.self) { park in
ParkDetailView(park: park)
}
この例だと、
NavigationLinkのvalueにPark型のオブジェクトが渡ってくると、
このParkDetailViewに遷移してくださいという意味になります。
NavigationLinkのvalueに渡ってくる型によって、どの画面に遷移するかを決定しているので、
型の数だけnavigationDestinationを生やせばよいです。
Case 3, Routerをenumにして遷移する例
StructだけでなくEnumでも良いです。
enumでcaseごとに定義しておいて、
navigationDestination内で遷移を定義してもよいです。
navigationDestinationのクロージャの引数としてenumのRouteが渡ってくるので、
それをswitchでどの画面に遷移するか定義すればよいです。
enum Route: Hashable {
case park(Park)
case fruits(Fruits)
}
NavigationStack {
List {
Section("Fruits List") {
ForEach(fruitsList) { fruits in
NavigationLink(value: Route.fruits(fruits)) {
Text(fruits.name)
}
}
}
Section("Park List") {
ForEach(parkList) { park in
NavigationLink(value: Route.park(park)) {
Text(park.name)
}
}
}
}
.navigationTitle("Fruits Park")
.navigationDestination(for: Route.self) { route in
switch route {
case let .fruits(fruits):
FruitsDetailView(fruits: fruits)
case let .park(park):
ParkDetailView(park: park)
}
}
}
Case 4, NavigationStackのpathを使った遷移
NavigationStackは文字通りStack、つまり、Viewを上に重ねていっております。
pathに構造体Parkが遷移されればNavigationStackのpathで
遷移を操作していく。
そのPathからremoveLastすれば1つ前に戻り、
removeLast(2)すれば、2つ前に戻る。
以下のshowFirstParkを実行すれば、
奈良公園の遷移して、代々木公園に遷移する。
@State private var path: [Park] = []
var body: some View {
NavigationStack(path: $path) {
List(parkList) { park in
NavigationLink(value: park) {
Text(park.name)
}
}
.navigationDestination(for: Park.self) { park in
ParkDetailView(park: park)
}
.navigationTitle("公園リスト")
}
.onChange(of: path) { newValue in
print(newValue)
}
.onAppear {
showFirstPark()
}
}
func showFirstPark() {
path = [.init(name: "奈良公園"), .init(name: "代々木公園")]
}
Case 5, NavigationPathのpathを使った遷移
pathには、非同期処理できてBindingできる@Stateないしは@Publishedのものを設定する。
pathに設定するのは、画面遷移するときに使用する構造体やEnumよいです
enum Route: Hashable {
case park(Park)
case fruits(Fruits)
}
final class RouterEnum: ObservableObject {
@Published var path: [Route] = []
}
routerはNavigationPathでも良いし、enumにして定義してもよいです。
NavigationPathというのもあり、
前の例だとpathは構造体Parkに限定していましたが、
navigationDestinationで定義されているものであれば、遷移が可能となります。
@Environmentプロパティーラッパーを使えば、子の環境すべてに伝搬できるので、
final class RouterNavigationPath: ObservableObject {
@Published var path = NavigationPath()
}
struct NavigationStackSample6: View {
@EnvironmentObject var router: RouterNavigationPath
let parkList: [Park] = [Park(name: "メリケンパーク"), .init(name: "万博記念公園"), .init(name: "奈良公園"), .init(name: "代々木公園")]
var body: some View {
NavigationStack(path: $router.path) {
List(parkList) { park in
NavigationLink(value: park) {
Text(park.name)
}
}
.navigationDestination(for: Park.self) { park in
ParkDetailView2(park: park)
}
.navigationTitle("公園リスト")
}
.environmentObject(router)
}
}
遷移先の画面にも
@EnvironmentObject var router: RouterNavigationPath
と書くことで、遷移先のParkDetailView2のrouterからremoveLast()すれば、
戻ることができます。
struct ParkDetailView2: View {
let park: Park
@EnvironmentObject var router: RouterNavigationPath
var body: some View {
Group {
Text("\(park.name)にようこそ")
Button("戻る"){
router.path.removeLast()
}
}
.onAppear {
print("router", router)
}
}
}
あとがき
かなり自由度が上がっていて書きやすそうなのも良いですね、
がっつりNavigationStackを使って書いてみようとおもいます。
リファレンス
Youtubeのコードまとめて 調査する
First Look at the Navigation API in SwiftUI
https://www.youtube.com/watch?v=3Ur6kUStKRo&t
SwiftUI 4: Navigation Stacks (WWDC 2022) – iOS
https://www.youtube.com/watch?v=dZdHJQT7Z4Y
New Programmatic Navigation In SwiftUI 4.0 - Xcode 13 - WWDC 2022
https://www.youtube.com/watch?v=rm7-DkqKZCo&t
NavigationStack
https://developer.apple.com/documentation/swiftui/navigationstack
A First Look at SwiftUI NavigationStack, NavigationPath, and “navigationDestination”
https://betterprogramming.pub/a-first-look-at-swiftui-navigationstack-navigationpath-and-navigationdestination-3a9bbb300e08
Mastering NavigationStack in SwiftUI. Navigator Pattern.
https://swiftwithmajid.com/2022/06/15/mastering-navigationstack-in-swiftui-navigator-pattern/