Skip to content

第43天 项目 9 第一部分

今天我们将开启一个新的技术项目,重点将聚焦于“导航”(navigation)。这是 SwiftUI 中的一个领域,我们在上一个项目中只是简要提及,但它实际上是你将要构建的几乎所有应用程序的核心所在。

正如你将看到的,导航实际上分为两种主要类型:由用户交互驱动的导航,以及由你自行触发的“程序化”(programmatic)导航。这两种导航都很重要,你会发现自己会灵活地将二者结合使用。

稍后你将探索的一个内容是:使用导航与使用工作表(sheets)之间的区别。二者都很重要,你肯定也会经常用到它们,但在学习这些内容的过程中,我希望你仔细思考每种选择背后的意义,以及你为什么会选择其中一种而非另一种——就像约翰·麦克斯韦所说:“找到你的‘原因’(why),你就能找到你的‘方法’(way)。”

今天你需要学习三个主题,通过这些主题,你将掌握一种更智能、更灵活的导航方法,即使用导航展示值(navigation presentation values)和 navigationDestination() 修饰符。

  • 导航:简介
  • 简单 NavigationLink 存在的问题
  • 使用 navigationDestination() 智能处理导航

导航:简介

作者:Paul Hudson 2023年11月2日

在这个技术项目中,我们将深入研究 SwiftUI 中的导航功能——如何使用 NavigationStack 从一个屏幕跳转到另一个屏幕,无论是用户主动触发,还是我们希望在特定时间自动触发。

在掌握了这些基础知识后,我们将进一步学习更高级的导航内容,包括状态恢复(state restoration)——记住用户确切的导航位置,这样当你的应用程序在未来重新启动时,用户可以从之前离开的地方继续操作。

完成这些内容后,我们还将探讨一些自定义选项:修改导航栏的外观、合理定位按钮,甚至如何让用户根据需求更改导航标题。

要学习的内容很多,但学完所有内容后,你将对 SwiftUI 中的导航工作原理有更深入的理解,从而能在你所有的应用程序中充分利用这一功能。

请创建一个名为“Navigation”的新 App 项目,让我们开始学习吧……

作者:Paul Hudson 2023年11月2日

当你刚开始学习 SwiftUI,或者你的需求非常简单时,你可能会写出这样的导航代码:

swift
NavigationStack {
    NavigationLink("点击我") {
        Text("详情视图")
    }
}

有时候,这种写法完全没问题,但它隐藏着一个问题。要弄清楚这个问题,我们来创建一个真正的详情视图,并在其初始化器中打印一条消息:

swift
struct DetailView: View {
    var number: Int

    var body: some View {
        Text("详情视图 \(number)")
    }

    init(number: Int) {
        self.number = number
        print("正在创建详情视图 \(number)")
    }
}

现在,尝试在导航代码中使用这个视图:

swift
NavigationStack {
    NavigationLink("点击我") {
        DetailView(number: 556)
    }
}

运行这个项目,但不要点击导航链接——只需运行项目,然后查看 Xcode 的调试控制台区域,你应该会看到“正在创建详情视图 556”这条信息。

出现这种情况的原因是:只要导航内容显示在屏幕上,SwiftUI 就会自动创建一个详情视图实例。偶尔这样做问题不大,但如果我们处理的内容更复杂呢?例如,假设在包含 1000 行数据的列表中这样写:

swift
NavigationStack {
    List(0..<1000) { i in
        NavigationLink("点击我") {
            DetailView(number: i)
        }
    }
}

现在,当你滚动列表时,会看到大量DetailView 实例被创建,而且很多实例还会被多次创建。这会让 Swift 和 SwiftUI 做大量不必要的工作,所以当你处理动态数据(例如,需要以相同方式显示多个不同整数)时,SwiftUI 为我们提供了一个更好的解决方案:为导航链接附加一个展示值(presentation value)。

接下来,我们就来看看这个方案……

使用 navigationDestination() 智能处理导航

作者:Paul Hudson 2023年11月2日

在 SwiftUI 最简单的导航形式中,我们会在单个 NavigationLink 中同时提供标签(label)和目标视图(destination view),如下所示:

swift
NavigationStack {
    NavigationLink("点击我") {
        Text("详情视图")
    }
}

但对于更高级的导航场景,最好将目标视图与值(value)分开。这样一来,SwiftUI 只会在需要时加载目标视图。

要实现这一点,需要两个步骤:

  1. NavigationLink 附加一个值。这个值可以是任何你需要的类型——字符串(string)、整数(integer)、自定义结构体实例(custom struct instance)等等。不过,有一个要求:无论你使用哪种类型,该类型都必须遵循 Hashable 协议。
  2. 在导航栈(navigation stack)中附加 navigationDestination() 修饰符,告诉 SwiftUI 当接收到该数据时应该执行什么操作。

这两个步骤都是新内容,但一开始你可以暂时忽略 Hashable 协议的要求,因为 Swift 中大多数内置类型都已经遵循了 Hashable 协议。例如,IntStringDateURL、数组(arrays)和字典(dictionaries)都已遵循 Hashable 协议,所以你无需为它们额外处理。

接下来,我们先了解 navigationDestination() 修饰符,之后再详细探讨 Hashable 协议。

首先,我们可以创建一个包含 100 个数字的列表(List),为每个数字的导航链接附加一个展示值——相当于告诉 SwiftUI,我们希望导航到某个数字对应的视图。代码如下:

swift
NavigationStack {
    List(0..<100) { i in
        NavigationLink("选择 \(i)", value: i)
    }
}

不过,这段代码还不够完整。是的,我们已经告诉 SwiftUI,当点击“选择 0”时,我们希望导航到与数字 0 对应的视图,但我们还没有说明该如何展示这个数据。是用一段文本(text)、包含图片的垂直栈(VStack)、自定义的 SwiftUI 视图,还是其他形式呢?

这就是 navigationDestination() 修饰符的用武之地:我们可以用它来指定“当需要导航到一个整数时,应该执行以下操作……”

将代码修改为如下形式:

swift
NavigationStack {
    List(0..<100) { i in
        NavigationLink("选择 \(i)", value: i)
    }
    .navigationDestination(for: Int.self) { selection in
        Text("你选择了 \(selection)")
    }
}

这样一来,当 SwiftUI 尝试导航到任何 Int 类型的值时,会将该值传入 selection 常量中,而我们需要返回对应的 SwiftUI 视图来展示这个值。

提示: 如果你需要导航到多种不同类型的数据,只需添加多个 navigationDestination() 修饰符即可。实际上,这相当于在告诉 SwiftUI:“当需要导航到整数时,执行这个操作;当需要导航到字符串时,执行那个操作。”

这种方式对于处理大量数据(如导航到字符串、整数和 UUID 对应的视图)非常有效。但对于更复杂的数据(如自定义结构体),我们需要使用“哈希”(hashing)功能。

哈希是计算机科学中的一个术语,指的是将某些数据以一致的方式转换为更小的表示形式的过程。哈希常用于下载数据的场景:假设你在 Apple TV 上下载一部电影,这部电影可能有 10GB 左右,但你如何确保每一个数据片段都下载成功了呢?

通过哈希技术,我们可以将这部 10GB 的电影转换为一个短字符串(可能总共只有 40 个字符),这个字符串可以唯一标识该电影。哈希函数需要具备一致性,也就是说,如果我们在本地对电影进行哈希处理,并将结果与服务器上的哈希值进行比较,二者应该始终相同——而比较两个 40 字符的字符串,要比比较两个 10GB 的文件容易得多!

显然,我们无法对数据进行“反向哈希”处理,因为你不可能将仅 40 个字符的字符串还原成 10GB 的电影。但这没关系:关键在于每个数据片段的哈希值都应该是唯一的,并且具有一致性,这样我们每次对同一部电影进行哈希处理时,得到的哈希值都是相同的。

Swift 在内部大量使用 Hashable 协议。例如,当你使用集合(Set)而不是数组(Array)时,你放入集合中的所有元素都必须遵循 Hashable 协议。这也是集合比数组速度更快的原因:当你判断“集合中是否包含某个特定对象”时,Swift 会计算该对象的哈希值,然后在集合中搜索这个哈希值,而不是将该对象的每个属性与集合中的所有对象进行比较。

如果这一切听起来很复杂,请记住:Swift 中大多数内置类型都已经遵循了 Hashable 协议。而且,如果你创建的自定义结构体的所有属性都遵循 Hashable 协议,那么只需一个小小的修改,就能让整个结构体也遵循 Hashable 协议。

例如,下面这个结构体包含一个 UUID、一个字符串和一个整数:

swift
struct Student {
    var id = UUID()
    var name: String
    var age: Int
}

如果我们想让这个结构体遵循 Hashable 协议,只需像这样添加协议声明即可:

swift
struct Student: Hashable {
    var id = UUID()
    var name: String
    var age: Int
}

现在,我们的 Student 结构体已经遵循了 Hashable 协议,因此它可以像整数或字符串一样,与 NavigationLinknavigationDestination() 配合使用。

本站使用 VitePress 制作