细品Swift - Navigation Stack
NavigationStack是苹果在2022年WWDC大会推出的一项重要ios16 swiftui特性。 它是NavigationView的替代,解决了其使用起来的一些问题,并且更大强大和灵活。
总览
NS通过让导航链接的值和具体的导航行为分离,使得导航行为更加容易控制。另外引入了导航路径的概念,让用户可以任意压入/弹出/替代导航路径,是的导航控制极为灵活。
简单替代NavigationView
首先,可以直接用NavigationStack替代NavigationView,程序效果没有任何变化。 如下面程序所示。
// MARK: - 用NavigationStack可以简单直接的替换NavigationView
// NavigationView{
NavigationStack {
List {
NavigationLink("Apple") {
Text("Here is an apple")
}
NavigationLink("Banana") {
Text("Here is a banana")
}
NavigationLink("Lemon") {
Text("Here is a lemon")
}
}
.navigationTitle("Fruits")
}
}
导航值和导航目标分离/目标集中处理
NavigationView被人诟病的问题之一是它在控制链接目标时难于使用。在编程控制链接目标方面,NavigationStack的结构更加清晰明确。后者把链接目标控制从每个link语句中抽出来,放在语句中进行统一控制。
.navigationDestination(for: <#T##Hashable.Protocol#>, destination: <#T##(Hashable) -> View#>)
请看下面的示例。每个链接目标语句都附有一个value值。可以这样理解,当一个链接被点击时,value值被记下,并转到navigationDestination处进行处理,在处理时传入当前的value值。 navigationDestination第一个参数for说明可以接受的value值的类型;第二个参数是个返回View的closure,传入被点击的value值,并返回基于这个value值计算出的新View,作为链接目标的下一个页面返回。
NavigationStack {
List {
// 每个link对应一个Hashable的值,这里用的是简单的String。enum值也经常被使用。
NavigationLink("Apple2", value: "Apple")
NavigationLink("Banana2", value: "Banana")
NavigationLink("Lemon2", value: "Lemon")
}
// 在这里统一处理链接目标。第一个参数for:说明可以接受的value值的类型;第二个参数是个返回View的closure。传入某个value值,并返回基于这个value值计算出的新View,作为链接目标返回。
.navigationDestination(for: String.self) { fruit in
Text("Link to \(fruit)")
}
}
NavigationStack使链接目标的控制集中在一处,编程控制起来更清晰。这里是更复杂一点的示例,value是enum类型。
enum Stationary: String {
case notebook, pencil, ruler
}
// MARK: - NavigationStack使链接目标的控制集中在一处,编程控制起来更清晰。这里是更复杂一点的示例,value是enum类型。
NavigationStack {
List {
// 每个link对应一个Hashable的值,这里用的是简单的String。enum值也经常被使用。
NavigationLink("Notebook", value: Stationary.notebook)
NavigationLink("Pencil", value: Stationary.pencil)
NavigationLink("Ruler", value: Stationary.ruler)
}
// 在这里统一处理链接目标。第一个参数for:说明可以接受的value值的类型;第二个参数是个返回View的closure。传入某个value值,并返回基于这个value值计算出的新View,作为链接目标返回。
.navigationDestination(for: Stationary.self) { stationary in
Text("Link to \(stationary.rawValue)")
}
}
这里再介绍一种更复杂的情况: 在一个NavigationStack中存在两种或以上的值类型,则根据各自的值类型去寻找链接目标。
// MARK: - 混合值类型。 NavigationStack使链接目标的控制集中在一处,编程控制起来更清晰。这里是更更复杂一点的示例,两种Value类型的混合: String和自定义enum类型。
NavigationStack {
List {
// 两种值类型。点击时各自寻找处理自己值类型的navigationDestination
NavigationLink("Notebook", value: Stationary.notebook)
NavigationLink("Pencil", value: Stationary.pencil)
NavigationLink("Ruler", value: Stationary.ruler)
NavigationLink("Apple2", value: "Apple")
NavigationLink("Banana2", value: "Banana")
NavigationLink("Lemon2", value: "Lemon")
}
//需要提供对应多种值类型的导航目标
.navigationDestination(for: Stationary.self) { stationary in
Text("Link to \(stationary.rawValue)")
}
.navigationDestination(for: String.self) { fruit in
Text("Link to \(fruit)")
Image(systemName: "leaf.fill")
}
}
最后提示一下,以下方法是之前NavigationView中使用的,都已经被苹果标为过时,不建议使用。
NavigationLink(destination: <#T##_#>, tag: <#T##Hashable#>, selection: <#T##Binding<Hashable?>#>, label: <#T##() -> _#>)
NavigationLink(<#T##title: StringProtocol##StringProtocol#>, tag: <#T##Hashable#>, selection: <#T##Binding<Hashable?>#>, destination: <#T##() -> _#>)
导航路径(path)让导航控制更为灵活
从上一小节我们看到,navigationDestination让编程控制链接目标方便处理。但NavigationStack提供给我们的不仅如此, 它还提供了控制导航页面堆栈的path工具,从另一个维度让导航控制更加强大。path的使用方法有两种,区分是堆栈中的值是否为同一种类型。 如果是同一种类型,可以用一个数组来标识path。否则,需要用到NavigationPath类型。
- 链接值为同一类型,可以用一个数组来表示路径(堆栈)。
- 链接值非同一类型,必须用NavigationPath来表示路径(堆栈)。
首先的范例是同类型的路径堆栈。
@State var sameValueTypePath: [Stationary] = []
// MARK: - 同类型的路径堆栈
// 首先,在NavigationStack上加入path参数。
NavigationStack(path: $sameValueTypePath) {
List {
// 符合path数组中类型的链接。
NavigationLink("Notebook For Path", value: Stationary.notebook)
NavigationLink("Pencil For Path", value: Stationary.pencil)
NavigationLink("Ruler For Path", value: Stationary.ruler)
// 不符合path数组中类型的链接,点击无效
NavigationLink("Something Weird", value: "Weird")
// 向path中加入两个值,即向导航堆栈中压入两个页面。
// 点击此按钮后进入pencil页面,点击back返回键,会发现pencil页面返回到ruler页面,然后才是根页面。
Button("Add something to path") {
sameValueTypePath.append(.ruler)
sameValueTypePath.append(.pencil)
}
}
.navigationDestination(for: Stationary.self) { stationary in
VStack {
Text("Link to \(stationary.rawValue)")
Text(sameValueTypePath.map(\.rawValue).joined(separator: "/"))
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
// 通过清空path,让导航堆栈清空,直接回到根导航页面。
Button("Return to root") {
sameValueTypePath = []
}
}
}
}
}
NavigationStack中可能是最强大的功能就是对混合路径堆栈的自由控制。请看以下范例:
// MARK: - 不同类型值的混合路径堆栈
// 首先,在NavigationStack上加入path参数。
NavigationStack(path: $hybridValueTypePath) {
List {
// 任意值类型的链接都可以加入path
NavigationLink("Notebook For Path", value: Stationary.notebook)
NavigationLink("Pencil For Path", value: Stationary.pencil)
NavigationLink("Ruler For Path", value: Stationary.ruler)
//
NavigationLink("Apple2", value: "Apple")
NavigationLink("Banana2", value: "Banana")
NavigationLink("Lemon2", value: "Lemon")
// 向path中加入类型不完全相同的三个值,即向导航堆栈中压入三个页面。
// 点击此按钮后进入pencil页面,点击back返回键,会发现页面依次返回:
// pencil -> Apple -> ruler -> 根页面
Button("Add something to path") {
hybridValueTypePath.append(Stationary.ruler)
hybridValueTypePath.append("Apple")
hybridValueTypePath.append(Stationary.pencil)
}
}
// 自定义enum类型Stationary的链接目标
.navigationDestination(for: Stationary.self) { stationary in
VStack {
Text("Link to \(stationary.rawValue)")
Text(String(hybridValueTypePath.count))
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
// 通过让导航堆栈清空,直接回到根导航页面。
Button("Return to root") {
hybridValueTypePath.removeLast(hybridValueTypePath.count)
}
}
}
}
// String类型的链接目标
.navigationDestination(for: String.self) { fruit in
Text("Link to \(fruit)")
Image(systemName: "leaf.fill")
}
}
总结起来,NavigationStack比前身NavigationView更为强大,提供了灵活的页面栈操作,而且可以处理混合导航值类型。