看官方文档说明吧: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用起来吧。