
SwiftUI中NavigationStack使用以及与NavigationView的区别(NavigationLink,navigationDestination,NavigationPath)
在iOS开发中,导航视图无疑是最常用的组件之一。当SwiftUI首次发布时,它附带了一个名为NavigationView的视图,用于构建基于导航的用户界面。随着iOS 16的发布,苹果已经弃用了旧的导航视图,并引入了一个名为NavigationStack的新视图来呈现视图堆栈。最重要的是,开发人员可以利用这个新视图来构建数据驱动的导航。
在iOS开发中,导航视图无疑是最常用的组件之一。当SwiftUI
首次发布时,它附带了一个名为NavigationView
的视图,用于构建基于导航的用户界面。随着iOS 16的发布,苹果已经弃用了旧的导航视图,并引入了一个名为NavigationStack
的新视图来呈现视图堆栈。最重要的是,开发人员可以利用这个新视图来构建数据驱动的导航。
在iOS 16及以后,NavigationView
将会被弃用,取而代之则是NavigationStack
。
首先,我们将研究如何实现NavigationView
。接下来,我们将看一个如何实现NavigationStack
的示例。
NavigationView
先看一下NavigationView
的用法:
struct NavigationStackDemo: View {
let colors: [Color] = [.red, .gray, .green, .orange, .pink, .brown, .cyan, .indigo, .purple, .yellow]
var body: some View {
NavigationView {
List(colors, id: \.self) { color in
NavigationLink {
ColorView(color: color)
} label: {
Text("\(color.description.capitalized)")
}
}
.listStyle(.plain)
.navigationTitle("NavigationView")
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct ColorView: View {
let color: Color
init(color: Color) {
self.color = color
print("\(color.description)")
}
var body: some View {
color
.ignoresSafeArea()
}
}
上面的代码采用NavigationView
和NavigationLink
的组合,加上List
组件,显示了一组颜色,并且点击的时候调转到另一个界面显示该颜色。
当我们用模拟器或者真机测试的时候,我们要跳转的ColorView
都已经创建出来了,我们在ColorView
的init
方法里面添加了打印。运行起来的结果看下面gif中的输出部分。
这种提前创建出来并不友好,上面我们是用了有复用机制的List
组件,那么提前创建出来的ColorView
实例并不多,但是如果用其他组件渲染,可能对内存有一定的影响。
NavigationStack
随着iOS 16 + 引入了NavigationStack
,为了兼容之前的NavigationView
,我们可以直接把NavigationView
换成NavigationStack
,其他的不变。
struct NavigationStackDemo: View {
let colors: [Color] = [.red, .gray, .green, .orange, .pink, .brown, .cyan, .indigo, .purple, .yellow]
var body: some View {
NavigationStack {
List(colors, id: \.self) { color in
NavigationLink {
ColorView(color: color)
} label: {
Text("\(color.description.capitalized)")
}
}
.listStyle(.plain)
.navigationTitle("NavigationView")
.navigationBarTitleDisplayMode(.inline)
}
}
}
不过如果只是这种改变,那我们就没有发挥出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的文本。
还是上面的示例,我们改一下代码,如下:
struct NavigationStackDemo: View {
let colors: [Color] = [.red, .gray, .green, .orange, .pink, .brown, .cyan, .indigo, .purple, .yellow]
var body: some View {
NavigationStack {
List(colors, id: \.self) { color in
NavigationLink(value: color) {
Text("\(color.description.capitalized)")
}
}
.listStyle(.plain)
.navigationTitle("NavigationView")
.navigationBarTitleDisplayMode(.inline)
.navigationDestination(for: Color.self) { color in
ColorView(color: color)
}
}
}
}
上面NavigationLink
的value
传入了color
值,当点击的时候,如果SwiftUI
在包含它的NavigationStack
的视图层次结构中找到了一个匹配的修饰符(navigationDestination
修饰符绑定的类型和NavigationLink
的value
的类型相同),它就会把这个修饰符对应的目标视图推入到堆栈中。
如果没有匹配的navigationDestination
修饰符,那么无法执行跳转。
navigationDestination
修饰符的构造器闭包中返回的参数即是NavigationLink
的value
。
另外采用这种方式,之前NavigationView
提前创建目标视图的问题也没有了。
多navigationDestination处理
如果List中有不同的类型数据,那么怎么支持跳转呢?
navigationDestination
修饰符可以添加一次,也可以添加多次,只要绑定不同的类型即可。比如上面的代码中我们在添加水果信息。
struct NavigationStackDemo: 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
ColorView(color: color)
}
.navigationDestination(for: String.self) { fruit in
FruitView(fruit: fruit)
}
}
}
}
代码中除了.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添加到数据中。
如果初始化一个空数组,那代码导航栏堆栈中没有任何视图。
struct NavigationStackDemo: View {
let colors: [Color] = [.red, .gray, .green, .orange, .pink, .brown, .cyan, .indigo, .purple, .yellow]
@State private var path: [Color] = []
let fruits: [String] = ["apple", "banana", "orange"]
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
ColorView(color: color)
}
.navigationDestination(for: String.self) { fruit in
FruitView(fruit: fruit)
}
}
}
}
在初始化的时候,我们也可以给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
数组中追加即可。
struct NavigationStackDemo: View {
let colors: [Color] = [.red, .gray, .green, .orange, .pink, .brown, .cyan, .indigo, .purple, .yellow]
// @State private var path: [Color] = [.red, .gray, .green, .orange]
@State private var path = NavigationPath()
let fruits: [String] = ["apple", "banana", "orange"]
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
ColorView(color: color)
}
.navigationDestination(for: String.self) { fruit in
FruitView(fruit: fruit)
}
}
.onAppear {
path.append("apple")
path.append(Color.red)
}
}
}
在onAppear
中先后添加了两个元素,程序运行起来后,导航栏直接push到了red界面,back后到apple界面,再back到主界面。
写在最后
NavigationStack
比NavigationView
要强大了很多,允许我们手动管理导航栏堆栈,如果我们的App最低支持iOS 16,那么就将NavigationStack
用起来吧。
最后,希望能够帮助到有需要的朋友,如果您觉得有帮助,还望点个赞,添加个关注,笔者也会不断地努力,写出更多更好用的文章。
更多推荐
所有评论(0)