第79天 项目 16 第一部分
在这个项目中,你将学习 Swift 和 SwiftUI 的一系列实用特性组合;这将切实帮助你提升技能,让你具备开发功能强大的应用程序的能力。没错,即便在我们 100 天学习计划的第 79 天,仍然有很多新知识需要学习——就像莉莉·汤普林(Lily Tomlin)所说:“成功之路始终在建设之中”!
不过需要提醒你:这个项目的内容相当多。我已经尝试将其拆分成更简短、更易懂的每日学习内容,以便你更好地跟上进度,但如果你觉得需要学习的内容太多而感到气馁,也请不要灰心——确实有很多内容要学!
今天你需要学习三个主题,分别是列表选择、标签视图等相关知识。
- 潜在客户(Hot Prospects):项目介绍
- 让用户在列表(List)中选择项目
- 使用 TabView 和 tabItem() 创建标签
潜在客户(Hot Prospects):项目介绍
作者:Paul Hudson 2024年2月1日
在这个项目中,我们将开发一款名为“潜在客户(Hot Prospects)”的应用程序,用于记录你在会议上结识的人员信息。你可能之前见过类似的应用:它会显示一个包含你参会者信息的二维码,其他人扫描该二维码后,就能将你添加到他们的潜在客户列表中,以便后续跟进。
这听起来可能不算复杂,但在开发过程中,我们会涵盖大量非常重要的新技巧:创建标签栏和上下文菜单、使用环境(environment)共享自定义数据、在锁屏界面显示通知等等。最终完成的应用程序会非常出色,但你在开发过程中学到的知识将更具实用价值!
和往常一样,在开始项目实现之前,我们有很多技巧需要学习,所以请先使用“App”模板创建一个新的 iOS 项目,并将其命名为“Hot Prospects”。
让我们开始吧!
让用户在列表(List)中选择项目
作者:Paul Hudson 2024年2月1日
通常我们会在列表行(List row)中放置一个 NavigationLink,这样用户点击行时就能查看更多信息,但有时你需要更多控制权——你希望点击操作仅用于“选择”一个项目,以便之后执行某种操作。
首先,以下是一个简单的列表,它不支持选择功能,仅显示多个字符串:
struct ContentView: View {
let users = ["透(Tohru)", "由希(Yuki)", "夹(Kyo)", "桃城(Momiji)"]
var body: some View {
List(users, id: \.self) { user in
Text(user)
}
}
}要让列表支持选择功能,需要执行三个步骤,首先要创建一个状态(state)来存储被点击的行。由于我们的列表显示的是字符串,因此选中的值应该是一个“可选字符串”——默认情况下没有任何项目被选中,但当用户点击某一行时,该属性将存储对应的用户名。
因此,我们需要在视图中添加以下属性:
@State private var selection: String?接下来,我们需要告诉列表,当用户点击某一行时更新该状态。这是一种双向绑定(two-way binding),意味着点击行会更新属性,而更新属性也会选中对应的行。
具体代码如下:
List(users, id: \.self, selection: $selection) { user in
Text(user)
}第三步,我们现在可以使用这个选中的结果了。例如,如果有选中的项目,我们可以在列表下方显示一段文本:
if let selection {
Text("你选中了 \(selection)")
}如果你想将其升级为支持“多选”功能,需要修改 selection 属性,使其存储一个集合(set)类型的值。默认情况下集合为空,表示没有任何项目被选中:
@State private var selection = Set<String>()在“显示”选中结果时,我们可以调用集合的 formatted() 方法,将所有选中的名称合并成一个字符串显示:
if selection.isEmpty == false {
Text("你选中了 \(selection.formatted())")
}当然,这里真正的难点在于如何启用多选模式,因为默认情况下点击一行会自动取消之前选中的行。
iOS 有一个相对隐蔽的手势可以激活多选模式:用两根手指在数据列表上水平滑动,就能激活多选。如果你使用模拟器,需要按住 Option 键来启用双指模式,然后还要按住 Shift 键来启用滑动功能,之后在列表上从左向右滑动即可。
虽然这两种方法都能生效,但它们都不够直观。一个“更好”的方法是添加一个 EditButton,它会自动处理编辑模式的启用和禁用,从而也能启用或禁用多选模式。因此,将以下代码添加到你的布局中:
EditButton()现在你可以自由地进入和退出多选模式,然后点击列表行旁边的复选框,将项目添加到选中集合中。至于选中项目后要执行什么操作,就由你决定了!
使用 TabView 和 tabItem() 创建标签
作者:Paul Hudson 2024年2月1日
导航栈(NavigationStack)非常适合创建具有层级结构的视图栈,让用户可以深入查看数据,但对于展示不相关的数据,它的效果并不好。这时我们就需要使用 SwiftUI 的 TabView,它会在屏幕底部创建一个按钮栏,点击每个按钮就能显示不同的视图。
在 TabView 中添加标签非常简单,只需逐个列出视图即可,如下所示:
TabView {
Text("标签1")
Text("标签2")
}但在实际开发中,你总是希望自定义标签的显示方式——在上面的代码中,标签栏会是一片空白的灰色区域。虽然你可以点击灰色区域的左侧和右侧来激活两个标签,但这种用户体验非常糟糕。
相反,更好的做法是为 TabView 中的每个视图添加 tabItem() 修饰符。这能让你自定义视图在标签栏中的显示方式,例如设置一个图像和一段文本,具体代码如下:
TabView {
Text("标签1")
.tabItem {
Label("第一个", systemImage: "star")
}
Text("标签2")
.tabItem {
Label("第二个", systemImage: "circle")
}
}除了让用户通过点击标签项切换视图外,SwiftUI 还允许我们通过状态(state)以编程方式控制当前显示的视图。这需要执行四个步骤:
- 创建一个 @State 属性,用于跟踪当前显示的标签。
- 每当需要跳转到不同标签时,将该属性修改为对应的值。
- 将该属性作为绑定(binding)传递给 TabView,以便自动跟踪其变化。
- 告诉 SwiftUI,对于该属性的每个值,应该显示哪个标签。
前三个步骤非常简单,我们先来完成它们。首先,需要一个状态来跟踪当前标签,因此在 ContentView 中添加以下属性:
@State private var selectedTab = "第一个"其次,需要在某个地方修改该属性,从而让 SwiftUI 切换标签。在我们的简单演示中,可以将文本改为按钮,代码如下:
Button("显示标签2") {
selectedTab = "第二个"
}
.tabItem {
Label("第一个", systemImage: "star")
}第三,需要将 TabView 的选择状态绑定到 $selectedTab。在创建 TabView 时将其作为参数传递,因此将代码更新为:
TabView(selection: $selectedTab) {现在来看关键部分:当我们执行 selectedTab = "第二个" 时,SwiftUI 如何知道这代表哪个标签呢?你可能会认为标签可以被视为一个数组,第二个标签的索引为 1,但这种方式会带来很多问题:如果我们将标签在 TabView 中移动到其他位置怎么办?
从更深层次来说,这还违背了 SwiftUI 的一个核心概念:我们应该能够自由组合视图。如果按钮能够通过“跳转到数组中的第二个标签”来切换视图,就意味着它对其父视图(TabView)的配置有深入的了解——它必须知道父视图的确切标签结构。
这是一种非常不好的做法,因此 SwiftUI 提供了更好的解决方案:我们可以为每个视图附加一个唯一的标识符,并用该标识符来指定选中的标签。这些标识符被称为“标签(tag)”,可以通过 tag() 修饰符来附加,代码如下:
Text("标签2")
.tabItem {
Image(systemName: "circle")
Text("第二个")
}
.tag("第二个")因此,我们完整的视图代码如下:
struct ContentView: View {
@State private var selectedTab = "第一个"
var body: some View {
TabView(selection: $selectedTab) {
Button("显示标签2") {
selectedTab = "第二个"
}
.tabItem {
Label("第一个", systemImage: "star")
}
.tag("第一个")
Text("标签2")
.tabItem {
Label("第二个", systemImage: "circle")
}
.tag("第二个")
}
}
}现在这段代码可以正常工作了:你可以通过点击标签项来切换标签,也可以通过激活第一个标签中的按钮来切换标签。
当然,仅使用“第一个”和“第二个”这样的字符串并不理想——这些值是固定的,虽然能解决视图位置移动的问题,但不容易记忆。幸运的是,你可以使用任何喜欢的值:为每个视图设置一个唯一且能体现其用途的字符串标签,然后将该标签用于 @State 属性。从长期来看,这种方式更易于使用,并且比使用整数更推荐。
提示: 通常我们会需要同时使用 NavigationStack 和 TabView,但需要注意一点:TabView 应该作为父视图,根据需要在其内部的标签中添加 NavigationStack,而不是反过来。