NavigationView已经过时了,该用 NavigationStack 和 NavigationSplitView了

4,927 阅读5分钟

看官方文档说明吧:NavigationView | Apple Developer Documentation 

在iOS 16及以后,NavigationView将会被弃用,取而代之则是NavigationStack。

NavigationStack

随着iOS 16 + 引入了NavigationStack,为了兼容之前的NavigationView,我们可以直接把NavigationView换成NavigationStack,其他的不变。

//
//  StackView.swift
//  SwiftBook
//
//  Created by song on 2024/7/4.
//

import SwiftUI

struct StackView: View {
    let colors: [Color] = [.red, .gray, .green, .orange, .pink, .brown, .cyan, .indigo, .purple, .yellow]

    var body: some View {
        NavigationStack {
            List(colors, id: \.self) { c in
                NavigationLink {
                    Color(c)
                } label: {
                    Text("\(c.description.capitalized)")
                }
            }
            .listStyle(.plain)
            .navigationTitle("NavigationView")
            .navigationBarTitleDisplayMode(.inline)
        }
    }
}

#Preview {
    StackView()
}

不过如果只是这种改变,那我们就没有发挥出NavigationStack的最大好处。

NavigationStack引入了一个名为navigationDestination修饰符,它将目标视图与呈现的数据类型关联起来。

func navigationDestination<D, C>(
    for data: D.Type,
    @ViewBuilder destination: @escaping (D) -> C
) -> some View where D : Hashable, C : View

data: 和目标视图匹配的数据类型。比如上面示例中,遍历colors数组,点击再跳转到目标界面,那么这个匹配的数据类型就是Color.self.
destination: 一个视图构造器,返回一个目标视图。当导航栏堆栈状态中包含了data类型的值,那么就显示这个目标视图。这个构造器带了一个data类型的参数。

如果使用了navigationDestination修饰符,那么在NavigationLink中我们也不需要添加目标视图了。而是采用下面这个NavigationLink初始化方法:

init<P>(
    value: P?,
    @ViewBuilder label: () -> Label
) where P : Hashable

value: 一个可选的值,当用户点击的时候,SwiftUI保存一个该value的副本,当传入nil的时候,界面也销毁了。

label:一个描述当前navigation link的文本。

还是上面的示例,我们改一下代码,如下:

//
//  StackView.swift
//  SwiftBook
//
//  Created by song on 2024/7/4.
//

import SwiftUI

struct StackView: View {
    let colors: [Color] = [.red, .gray, .green, .orange, .pink, .brown, .cyan, .indigo, .purple, .yellow]

    var body: some View {
        NavigationStack {
            List(colors, id: \.self) { c in
                NavigationLink(value: c) {
                    Text("\(c.description.capitalized)")
                }
            }
            .listStyle(.plain)
            .navigationTitle("NavigationView")
            .navigationBarTitleDisplayMode(.inline)
            .navigationDestination(for: Color.self, destination: { c in
                Color(c)
            })
        }
    }
}

#Preview {
    StackView()
}

上面NavigationLink的value传入了color值,当点击的时候,如果SwiftUI在包含它的NavigationStack的视图层次结构中找到了一个匹配的修饰符(navigationDestination修饰符绑定的类型和NavigationLink的value的类型相同),它就会把这个修饰符对应的目标视图推入到堆栈中。
如果没有匹配的navigationDestination修饰符,那么无法执行跳转。

navigationDestination修饰符的构造器闭包中返回的参数即是NavigationLink的value。

另外采用这种方式,之前NavigationView提前创建目标视图的问题也没有了。

多navigationDestination处理
如果List中有不同的类型数据,那么怎么支持跳转呢?

navigationDestination修饰符可以添加一次,也可以添加多次,只要绑定不同的类型即可。比如上面的代码中我们在添加水果信息:

//
//  StackView.swift
//  SwiftBook
//
//  Created by song on 2024/7/4.
//

import SwiftUI

struct StackView: View {
    let colors: [Color] = [.red, .gray, .green, .orange, .pink, .brown, .cyan, .indigo, .purple, .yellow]

    let fruits: [String] = ["apple", "banana", "orange"]

    var body: some View {
        NavigationStack {
            List {
                Section("Colors") {
                    ForEach(colors, id: \.self) { color in
                        NavigationLink(value: color) {
                            Text("\(color.description.capitalized)")
                        }
                    }
                }
                Section("Fruits") {
                    ForEach(fruits, id: \.self) { fruit in
                        NavigationLink(value: fruit) {
                            Text("\(fruit.capitalized)")
                        }
                    }
                }
            }
            .listStyle(.plain)
            .navigationTitle("NavigationView")
            .navigationBarTitleDisplayMode(.inline)
            .navigationDestination(for: Color.self) { color in
                Color(color)
            }
            .navigationDestination(for: String.self) { fruit in
                Text(fruit)
            }
        }
    }
}

#Preview {
    StackView()
}

代码中除了.navigationDestination(for: Color.self),还添加了.navigationDestination(for: String.self),显示水果的时候,我们用的是String类型,NavigationLink中也绑定了对应的水果值,如:NavigationLink(value: fruit)。

 

特别提示:

不要将navigationDestination修饰符添加到懒加载容器控件的内部,比如List或者LazyVStack等,这些懒加载容器控件只在需要在屏幕上呈现子视图时才创建子视图。必须在这些懒加载容器控件外部添加navigationDestination修饰符,以便导航堆栈始终可以看到目的地。 

导航堆栈管理状态(Navigation state)
默认情况下,导航堆栈管理状态以跟踪堆栈上的视图。但是,我们的代码可以通过绑定到创建的数据值集合来初始化堆栈,从而实现对状态的控制。堆栈在向堆栈添加视图时向集合添加元素,并在删除视图时删除元素。
NavigationStack视图有另一个初始化方法,它接受一个path参数,该参数绑定到堆栈的导航状态。

@MainActor
init(
    path: Binding<Data>,
    @ViewBuilder root: () -> Root
) where Data : MutableCollection, Data : RandomAccessCollection, Data : RangeReplaceableCollection, Data.Element : Hashable

下面代码中添加了一个名为path的状态变量,它是一个Color数组,用于记录导航状态。在NavigationStack的初始化过程中,我们传递并绑定它来管理堆栈。当导航堆栈的状态发生变化时,path变量的值将自动更新,比如点击red进入到下一个界面后,path数组就将red添加到数据中。
如果初始化一个空数组,那代码导航栏堆栈中没有任何视图。

//
//  StackView.swift
//  SwiftBook
//
//  Created by song on 2024/7/4.
//

import SwiftUI

struct StackView: View {
    let colors: [Color] = [.red, .gray, .green, .orange, .pink, .brown, .cyan, .indigo, .purple, .yellow]

    let fruits: [String] = ["apple", "banana", "orange"]
    
    @State private var path: [Color] = []

    var body: some View {
        NavigationStack(path: $path) {
            List {
                Section("Colors") {
                    ForEach(colors, id: \.self) { color in
                        NavigationLink(value: color) {
                            Text("\(color.description.capitalized)")
                        }
                    }
                }
                Section("Fruits") {
                    ForEach(fruits, id: \.self) { fruit in
                        NavigationLink(value: fruit) {
                            Text("\(fruit.capitalized)")
                        }
                    }
                }
            }
            .listStyle(.plain)
            .navigationTitle("NavigationView")
            .navigationBarTitleDisplayMode(.inline)
            .navigationDestination(for: Color.self) { color in
                Color(color)
            }
            .navigationDestination(for: String.self) { fruit in
                Text(fruit)
            }
        }
    }
}

#Preview {
    StackView()
}

在初始化的时候,我们也可以给path添加一些元素,这意味着导航栏堆栈中添加了这些对应的值,程序运行起来后直接就显示了堆栈顶部的值关联的界面。

上面的代码中,绑定的path是我们创建的@State private var path: [Color] = [],是一个具体数据类型的状态数组,这种情况下,如果List中显示了不同的类型的数据,那么只有与path类型相同的数据才能跳转到下一个界面,比如上面代码中,点击color就行跳转到下一个界面,而点击fruit则毫无反应。如果要解决这个问题,在初始化NavigationStack的时候,在传入绑定path的时候,传入一个NavigationPath类型的状态值。

@State private var path = NavigationPath()


@MainActor
init(
    path: Binding<NavigationPath>,
    @ViewBuilder root: () -> Root
) where Data == NavigationPath

这样就支持不同类型的跳转了。如果想往导航栏堆栈中提前添加一些元素,就直接往path数组中追加即可。

//
//  StackView.swift
//  SwiftBook
//
//  Created by song on 2024/7/4.
//

import SwiftUI

struct StackView: View {
    let colors: [Color] = [.red, .gray, .green, .orange, .pink, .brown, .cyan, .indigo, .purple, .yellow]

    let fruits: [String] = ["apple", "banana", "orange"]

//    @State private var path: [Color] = []

    @State private var path = NavigationPath()

    var body: some View {
        NavigationStack(path: $path) {
            List {
                Section("Colors") {
                    ForEach(colors, id: \.self) { color in
                        NavigationLink(value: color) {
                            Text("\(color.description.capitalized)")
                        }
                    }
                }
                Section("Fruits") {
                    ForEach(fruits, id: \.self) { fruit in
                        NavigationLink(value: fruit) {
                            Text("\(fruit.capitalized)")
                        }
                    }
                }
            }
            .listStyle(.plain)
            .navigationTitle("NavigationView")
            .navigationBarTitleDisplayMode(.inline)
            .navigationDestination(for: Color.self) { color in
                Color(color)
            }
            .navigationDestination(for: String.self) { fruit in
                Text(fruit)
            }
        }.onAppear {
            path.append("apple")
            path.append(Color.red)
        }
    }
}

#Preview {
    StackView()
}

在onAppear中先后添加了两个元素,程序运行起来后,导航栏直接push到了red界面,back后到apple界面,再back到主界面。

NavigationStack比NavigationView要强大了很多,允许我们手动管理导航栏堆栈,如果我们的App最低支持iOS 16,那么就将NavigationStack用起来吧。