我们将仔细研究 SwiftUI 中的导航 - 我们如何使用NavigationStack 从一个屏幕移动到另一个屏幕。
导航栏
使用NavigationStack包裹视图,即可出现导航栏并出现相关的效果,如标题,以及跳转的基础。
提示:很容易认为修饰符应该附加到 NavigationStack的末尾,但实际上是附加到Form的末尾。原因是NavigationStack能够在程序运行时显示许多视图,因此通过将标题附加到NavigationStack内的视图,我们允许 iOS 自由更改标题。
- 更改标题
使用绑定
@State private var title = "标题"
var body: some View {
NavigationStack {
Form{
Text("Hello, world!")
Text("Hello, world!")
Text("Hello, world!")
}
.navigationTitle($title)
.navigationBarTitleDisplayMode(.automatic)
}
}
相关修饰符
-
深浅色模式
.toolbarColorScheme(.dark) -
背景
.toolbarBackground(.blue) -
隐藏
.toolbar(.hidden, for: .navigationBar) -
隐藏返回按钮
.navigationBarBackButtonHidden() -
onChange 监听属性变化
.onChange(of: xxx ) { oldValue, newValue in
// 特定属性值发生变化,它就会运行我们选择的闭包
}
- 生命周期函数
.onAppear()
- 执行函数
.onSubmit() // 按下键盘上的 return 时调用
- 插入视图的安全区域,并在剩余空间中放置一些内容
.safeAreaInset(edge: .bottom) {
Text("显示")
.frame(maxWidth: .infinity)
.padding()
.background(.blue)
.foregroundStyle(.white)
.font(.title)
}
工具栏
- toolbar
toolbar()让我们可以指定视图的工具栏项。这些工具栏项可能出现在屏幕上的各个位置 - 顶部的导航栏中、底部的特殊工具栏区域等。
- 将工具栏按钮放置在精确位置
如果需要,您可以使用ToolbarItem 进行自定义。它围绕着您的工具栏按钮,允许您通过从多个选项之一中进行选择来将它们准确地放置在您想要的位置。
.toolbar{
ToolbarItem(placement: .topBarLeading) {
Button("编辑") {
//
}
}
}
ToolbarItem(placement: .destructiveAction) {
Button("跳转") {
}
}
如果您希望多个按钮 ,您重复使用ToolbarItem
或使用ToolbarItemGroup
.toolbar {
ToolbarItemGroup(placement: .topBarLeading) {
Button("Tap Me") {
}
Button("Tap Me 2") {
}
}
}
导航跳转页面
方式1:NavigationLink { }
当用户点击它时,我们向他们呈现一个新视图,使用NavigationLink:给它一个目的地和可以点击的东西,它会处理其余的事情。
NavigationStack {
NavigationLink("点击") {
Text("详情页") // 将跳转的页面
}
.navigationTitle("SwiftUI")
}
可自定义点击按钮
NavigationLink {
Text("Detail View")
} label: {
VStack {
Text("This is the label")
Text("So is this")
Image(systemName: "face.smiling")
}
.font(.largeTitle)
}
常用于列表 跳转:
在右侧边缘看到灰色的公开指示器箭头。这是标准的 iOS 方式,告诉用户当点击该行时另一个屏幕将从右侧滑入,并且 SwiftUI 足够智能,可以在此处自动添加它。如果这些行不是导航链接(如注释掉该NavigationLink行及其右大括号),您将看到指示器消失。
- NavigationLink的隐藏问题
NavigationLink("点击") {
Text("详情页") // 将跳转的页面
}
父级视图呈现时,其中的NavigationLink包裹的次级视图详情页,也会同时被创建,虽然并未发生跳转。特别是当发生在列表中,将会创建多个次级视图,情况看起来就更糟糕,因为这很浪费性能。
放心,SwiftUI 为我们提供了更好的解决方案。
方式2:value + navigationDestination
对于更高级的导航,最好将目的地与值分开。这允许 SwiftUI 仅在需要时 加载目标。
使用 navigationDestination() 以智能方式处理导航。需要两个步骤:
- 给 NavigationLink赋予一个值value。该值可以是您想要的任何值 - 字符串、整数、自定义结构实例或其他任何值。但是,有一个要求:无论您使用什么类型,都必须符合名为Hashable 的协议。
- 在导航堆栈中附加一个navigationDestination()修改器,告诉它在收到数据时要做什么。
示例
创建List100 个数字,每个数字都附加到一个导航链接作为其表示值 - 我们告诉 SwiftUI 我们想要导航到一个数字。
当 SwiftUI 尝试导航到任何Int值时,它会在常量selection中为我们提供该值,并且我们需要返回正确的 SwiftUI 视图来显示它。
大多数 Swift 的内置类型已经符合Hashable。如Int、String、Date、URL、UUID、 数组和字典等
但对于更复杂的数据(例如自定义结构体),我们需要使用哈希。
Hashable协议
如果您创建一个具有所有符合Hashable 的属性的自定义结构体,则可以通过使整个结构体符合Hashable。
struct Student: Hashable {
var id = UUID()
var name: String
var age: Int
}
于是,我们就可以根据数据类型进行跳转了
.navigationDestination(for: Student.self) { selection in
Text("你选择了 \(selection)")
}
编程式导航
上述的方式1和方式2,都需要经过用户点击才能发生跳转
编程式导航允许我们在需要的时候,仅使用代码从一个视图移动到另一个视图,而不是等待用户采取特定操作 。
方式3:Int数组 绑定path
首先,创建一个@State属性的 Int 数组path。然后绑定到NavigationStack,这意味着更改数组将自动导航到数组中的任何内容,而且当用户按导航栏中的“返回”时也会更改数组。
注意,在此仍然是借助navigationDestination()设定目标
当path数组方式改变,即可发生 页面的跳转。
还可以
Button("Show 32 then 64") {
path = [32, 64]
}
这将显示 32 的视图,然后显示 64 的视图,因此用户需要点击“返回”两次才能返回到根视图。
您可以根据需要混合用户导航和编程导航 - SwiftUI 将负责确保您的path数组与您显示的任何数据保持同步,无论其显示方式如何。
导航到不同的数据类型
当数据有多种类型时,怎么办?当然,你可以使用方式2,只需添加navigationDestination()多次,为您想要的每种数据类型添加一次 。如:
但如果需要 编程导航,且 需要 导航到不同的数据类型
怎么办??
方式4:NavigationPath + navigationDestination
SwiftUI 的解决方案是一种名为NavigationPath 的特殊类型,跟数组不同,它能够 同时保存多种数据类型。
NavigationPath存储任何类型的Hashable数据,而不暴露每个项目到底是什么类型的数据。 那就是我们所说的 类型擦除器
编程方式 返回根视图
场景:您的用户正在下订单,并通过显示购物车的屏幕进行操作,询问运输详细信息,询问付款详细信息,然后确认订单,但是当他们完成后,您想返回到最开始的界面 。
当然,如果是方式3,可以使用removeAll()清除 Int数组所有内容,然后返回到根视图;如果是方式4,可以重置path = NavigationPath()
问题是:当您无法访问原始path属性时,如何从子视图中做到这一点?
- 方式A:将路径path 存储在使用的 @Observable 外部类中
另外,您可以使用一个新属性包装器
@Binding
属性包装器 @Binding 允许我们将 @State 属性传递到另一个视图并从那里修改它——我们可以在多个地方共享一个 @State 属性,并且在一个地方更改它会在任何地方更改它。
父视图中定义@State 的 path 存储属性
@State private var path = [Int]()
子视图中,定义@Binding 的 path 存储属性
@Binding var path: [Int]
通过传递参数
DetailView(path: $path)
同理:NavigationPath()