编程式导航允许我们仅仅使用代码,即可实现从一个视图移动到另一个视图,而不是等待用户操作。例如,也许程序正忙于处理某些用户输入,并且我们希望在该工作完成后,再自动导航到结果屏幕。例如支付成功后,再跳转下一个页面。
在 SwiftUI 中,这是通过将 NavigationStack
的 path
路径参数绑定到【正在导航的任何数据的数组】来完成的。
<aside>
💡 可以根据需要混合用户导航和编程导航,SwiftUI 将确保 path
数组与您显示的任何数据保持同步,无论其显示方式如何。
</aside>
// 例如,使用整数数组 [Int] 作为导航数据
struct ContentView: View {
// 1. 创建一个 @State 属性来存储【整数数组】
@State private var path = [Int]()
var body: some View {
// 2. 将该属性绑定到 NavigationStack 的 path
// 这意味着【更改数组】将自动导航到数组中的任何内容,而且当用户按 Back 时也会更改数组导航栏
NavigationStack(path: $path) {
VStack {
// 设置整个数组仅包含数字 32(如果数组发生其他变化使 32 被删除,则意味着导航将返回到其原始状态)
Button("Show 32") { path = [32] }
// 给数组附加值 64,这意味着它将添加到导航到的任何内容中。(如果数组已经包含 32,那么现在在堆栈中将拥有3个视图:原始视图(称为“根”视图),然后是显示数字 32 的视图,最后是显示数字 64 的视图)
Button("Show 64") { path.append(64) }
// 同时推送多个值。这将显示32的视图,然后显示64的视图,因此用户要点击“返回”两次才能返回根视图
Button("Show 32 then 64") {
path = [32, 64]
}
}
.navigationDestination(for: Int.self) { selection in
Text("You selected \\(selection)")
}
}
}
}
当需要导航到的值可能是整数,也有可能是字符串时,就不能再使用简单的数组作为 NavigationStack
的绑定参数了。解决方案是使用名为 NavigationPath
的特殊类型,它能够在单个路径中保存多种数据类型。NavigationPath
就是所说的类型擦除器 ,它存储任何类型的 Hashable
数据,而不暴露每个项目的数据类型。
简而言之,导航到不同类型的数组,就用 NavigationPath
进行区分,NavigationPath
能够在单个路径对象中保存多种数据类型。
具体操作方式如下所示:
// 1. 新建一个名为 NavigationPath 特殊类型的状态参数
@State private var path = NavigationPath()
// 2. 绑定到导航视图
NavigationStack(path: $path) {...}
// 3. 以编程方式将内容推送到它,例如使用工具栏按钮:
.toolbar {
Button("Push 556") {
path.append(556)
}
Button("Push Hello") {
path.append("Hello")
}
}
// 例子2
struct ContentView: View {
@State private var navPath = NavigationPath()
var body: some View {
NavigationStack(path: $navPath) {
Button("Jump to random") {
navPath.append(Int.random(in: 1..<50))
}
List(1..<50) { i in
NavigationLink(value: "Row \\(i)") {
Label("Row \\(i)", systemImage: "\\(i).circle")
}
}
.navigationDestination(for: Int.self) { i in
Text("Int Detail \\(i)")
}
.navigationDestination(for: String.self) { i in
Text("String Detail \\(i)")
}
.navigationTitle("Navigation")
}
}
}
当你在 NavigationStack
中,进行了N次下钻的访问后,想要一次性回到最初的视图,有两种做法:
removeAll()
来删除路径中的所有内容,返回根视图path = NavigationPath()
当希望从子视图中操作上面的数组、或者 path
数据时,会遇到提示“无权访问原始的 path
属性”的问题,这里有两个选择:
@Observable
的外部类中;参见:@Observable 宏使用方法@Binding
属性包装器,和主视图的 path
进行绑定<aside>
💡 @State
可以在视图中创建一些存储,以便在程序运行时修改值。而 @Binding
属性包装器则允许将 @State
属性,传递到另一个视图并从那里修改它。这样我们可以在多个地方共享 @State
属性,并更改它在一个地方会改变它在任何地方。
</aside>
// 具体做法如下:
// 1. 需要在【主视图】使用 DetailView 的地方传递 path 参数(记得加 $ 美元符号)
DetailView(number: 0, path: $path)
.navigationDestination(for: Int.self) { i in
DetailView(number: i, path: $path)
}
// 2. 在【子视图】中添加新 @Binding 属性,接收传入的 path 数据,用以设置导航路径
@Binding var path: [Int]
// 3. 在【子视图】添加按钮来操作 path 路径数组
// 如果使用的是【简单数组】作为路径,则使用 removeAll 清空数组返回
.toolbar {
Button("Home") { path.removeAll() }
}
// 如果使用的是【NavigationPath】作为路径,将其设置为新的空实例
.toolbar {
Button("Home") { path = NavigationPath() }
}
//像这样共享绑定很常见,这正是 TextField 、 Stepper 和其他控件的工作方式。
当使用 NavigationPath
储存导航路径数据时,它可以通过 navigationDestination(for: Xxxxx.self)
去储存多种类型元素,如果想要判断其中是否包含特定类型的元素(例如 Project
类型),可以使用 NavigationPath
的 elements
属性,elements
是一个数组,包含了当前导航路径中的所有元素。你可以通过遍历这个数组来检查是否包含 Project
类型的元素。
struct HomePageView: View {
var body: some View {
NavigationStack(path: $homePagePath) {
// 在需要判断的地方
if !containsProject() {
// 这里可以放置不包含 Project 时的视图
Text("当前没有 Project")
}
}
}
// 方法:判断 homePagePath 中是否包含 Project 类型的元素
// 1. homePagePath.elements 访问 NavigationPath 的 elements 属性,获取当前导航路径中的所有元素
// 2. 使用 contains(where:) 方法来检查数组中是否存在 Project 类型的元素。这里使用了类型检查 is 来判断元素的类型
private func containsProject() -> Bool {
return homePagePath.elements.contains(where: { $0 is Project })
}
}
如果您使用同类数组(例如 [Int]
或 [String]
)作为导航路径数据,那么可以直接使用传统的 userDefaults
的方法,自由地加载或保存数据。参见:‣ 。
// 以下展示的是将导航路径数据保存到【文件中】的例子,没有用 UserDefault
// 首先创建储存导航路径的类 PathStore, 由于数据是要共享的,所以加上 @Observable 关键词
@Observable
class PathStore {
// 定义一个 path 参数, 类型是简单的整数数组
var path: [Int] {
//设置属性观察,当这个参数被设置时,调用 save 方法
didSet {
save()
}
}
// 设置保存路径
private let savePath = URL.documentsDirectory.appending(path: "SavedPath")
// 类需要自己做初始化
init() {
// 初始化时:从用户数据中读取以往存下来的导航数据
if let data = try? Data(contentsOf: savePath) {
//初始化时:将存下来的导航数据解码,赋值给 path 参数
if let decoded = try? JSONDecoder().decode([Int].self, from: data) {
path = decoded
return
}
}
//如果读取数据或者解码不成功,就将导航数据设置为空(默认)
path = []
}
//方法:每当path参数发生变化时,调用该方法将数据存入用户数据。
func save() {
do {
let data = try JSONEncoder().encode(path)
try data.write(to: savePath)
} catch {
print("Failed to save navigation data")
}
}
}
// 创建好类之后,接下来在视图代码中,将 NavigationStack 的路径绑定到 PathStore 实例的 path 属性
//【主视图】
struct ContentView: View {
//创建类的实例:作为状态参数
@State private var pathStore = PathStore()
var body: some View {
//绑定导航数据给NavigationStack
NavigationStack(path: $pathStore.path) {
DetailView(number: 0)
.navigationDestination(for: Int.self) { i in
DetailView(number: i)
}
}
}
}
//【子视图】
// 这个子视图代码,可以让你不断导航任意数量的子视图,你可以随时退出并重新启动应用程序,来测试导航数据是否能恢复为您离开时的状态:
struct DetailView: View {
var number: Int
var body: some View {
NavigationLink("Go to Random Number", value: Int.random(in: 1...1000))
.navigationTitle("Number: \\(number)")
}
}
如果使用 NavigationPath
来存储 NavigationStack
的导航路径,则可以用以下方法来保存和加载路径:
// 在前者基础上需要进行 4 个小更改:
@Observable
class PathStore {
// 1.Path 属性需要具有类型 NavigationPath,而不是 [Int]
var path: NavigationPath {
didSet {
save()
}
}
private let savePath = URL.documentsDirectory.appending(path: "SavedPath")
init() {
// 2.当在初始化器中解码 JSON 时,需要解码为特定类型,然后使用解码后的数据创建一个新的 NavigationPath
if let data = try? Data(contentsOf: savePath) {
if let decoded = try? JSONDecoder().decode(NavigationPath.CodableRepresentation.self, from: data) {
path = NavigationPath(decoded)
return
}
}
// 3.如果解码失败,应该将一个新的空 NavigationPath 实例分配给初始化程序末尾的 path 属性
path = NavigationPath()
}
// 4. save() 方法需要编写导航路径的 Codable 表示。这是与使用简单数组稍有不同的地方,因为 NavigationPath 不要求其数据类型符合 Codable,它只需要 Hashable 一致性。因此,Swift 无法在编译时验证导航路径是否存在有效的 Codable 表示
func save() {
// 这意味着在保存方法的开头,需要单独添加检查方法 .codable ,尝试检索 Codable 的导航路径,如果没有返回,则立即退出。这将返回准备编码为 JSON 的数据,或者如果路径中至少有一个对象无法编码,则返回 nil
guard let representation = path.codable else { return }
do {
//最后,检查通过后,将该 Codable 的表示形式 representation 转换为 JSON,而不是原始的 Int 数组:
let data = try JSONEncoder().encode(representation)
try data.write(to: savePath)
} catch {
print("Failed to save navigation data")
}
}
}
<aside>
💡 总结:以上两种方式都依赖于将【路径数据】存储在视图之外的类中,因此,路径数据的所有加载和保存都是在不可见的情况下发生的(外部类会自动处理它)。每当路径数据发生变化时([Int]、[String]、或 NavigationPath
对象),我们都需要保存新路径,以便将来保留它。并且当类被初始化时,我们还需要加载该数据。
</aside>