第82天 项目 16 第四部分
是时候将新学到的技巧付诸实践了,这个项目规模较大,需要三天天时间来完成实现。不过今天是第82天,事实已经证明你有毅力做出出色的成果——就像航空先驱阿米莉亚·埃尔哈特曾经说过的:“最困难的事情是下定决心行动,剩下的不过是坚持不懈。”
今天我们要回到SwiftData的学习中,因为这款这款这款应用非常适合用它来存储数据。我知道你可能会觉得我们只是在重复之前的内容,但请相信我:重复是学习过程中最重要的环节之一,而且我在今天和之前SwiftData相关内容的学习之间留出了不少天的间隔,特意这样安排是因为增加这段额外的时间有助于知识的巩固。(如果你好奇,这种方法叫做“间隔重复”——很管用的!)
好了,闲话不多说,今天你有很多任务要完成,我们赶紧开始看代码吧。
今天你需要完成三个主题的学习,我们会涵盖标签视图、SwiftData过滤等更多内容。
- 构建标签栏
- 使用SwiftData存储数据
- 动态过滤SwiftData查询结果
这款应用将在标签栏中显示四个SwiftUI视图:一个用于展示你认识的所有人,一个用于展示你已经联系过的人,另一个用于展示你尚未联系过的人,最后一个则展示你的个人信息,方便他人扫描。
前三个视图在概念上大同小异,但最后一个视图则完全不同。因此,我们只需用三个视图就能呈现所有的用户界面:一个用于展示联系人,一个用于展示个人信息,还有一个通过TabView将其他视图整合在一起。
所以,第一步我们要为各个标签创建占位视图,之后再回来完善它们。按下Cmd+N组合键,新建一个SwiftUI视图并命名为“ProspectsView”,然后再新建一个SwiftUI视图,命名为“MeView”。目前可以暂时保留这两个视图默认的“Hello, World!”文本视图,暂时不需要修改。
现在,关键在于ContentView,因为我们要在这个视图中创建TabView,并将所有其他视图包含在其中。很快我们会在这个视图中添加更多逻辑,但目前,它只需是一个包含三个ProspectsView实例和一个MeView实例的TabView即可。每个视图都会添加tabItem()修饰符,其中包含我从SF Symbols中挑选的图标和相应的文本。
将你当前ContentView的body部分替换为以下代码:
TabView {
ProspectsView()
.tabItem {
Label("所有人", systemImage: "person.3")
}
ProspectsView()
.tabItem {
Label("已联系", systemImage: "checkmark.circle")
}
ProspectsView()
.tabItem {
Label("未联系", systemImage: "questionmark.diamond")
}
MeView()
.tabItem {
Label("我的信息", systemImage: "person.crop.square")
}
}现在运行应用,你会看到屏幕底部出现一个简洁的标签栏,点击标签栏就能在四个视图之间切换。
显然,创建三个ProspectsView实例在实际使用中会显得很奇怪,因为它们完全相同,但我们可以通过自定义每个视图来解决这个问题。记住,我们希望第一个视图展示你认识的所有人,第二个展示你已经联系过的人,第三个展示你尚未联系过的人,要实现这一点,我们可以定义一个枚举,并在ProspectsView中添加一个相关属性。
现在,在ProspectsView内部添加以下枚举:
enum FilterType {
case none, contacted, uncontacted
}接下来,我们可以利用这个枚举,通过为ProspectsView添加一个新属性,让每个ProspectsView实例都有所不同:
let filter: FilterType这一修改会立即导致ContentView及其预览无法编译,因为创建ProspectsView实例时需要为该属性提供一个值。不过,我们先利用这个属性对三个视图进行一些自定义,为它们设置不同的导航栏标题。
首先,在ProspectsView中添加以下计算属性:
var title: String {
switch filter {
case .none:
"所有人"
case .contacted:
"已联系的人"
case .uncontacted:
"未联系的人"
}
}然后,将默认的“Hello, World!”文本视图替换为以下内容:
NavigationStack {
Text("Hello, World!")
.navigationTitle(title)
}这样至少能让每个ProspectsView实例在外观上有所区别,便于我们确认标签栏是否正常工作。
要让代码重新编译,我们需要确保调用每个ProspectsView初始化器时都传入一个filter值。因此,将预览代码修改为:
ProspectsView(filter: .none)接着,修改ContentView中的三个ProspectsView实例,分别为它们设置filter: .none、filter: .contacted和filter: .uncontacted。
现在运行应用,看起来会好很多。接下来,我们引入一些数据……
使用SwiftData存储数据
作者:Paul Hudson 2024年2月1日
很多应用都非常适合使用SwiftData,而且大多数情况下,只需很少的工作量就能完成相关设置。
在我们的应用中,TabView包含三个ProspectsView实例,我们希望这三个实例都能操作同一组共享数据。用SwiftData的术语来说,这意味着它们都访问同一个模型上下文(model context),但使用略有不同的查询条件。
首先,新建一个名为Prospect.swift的Swift文件,将文件中默认导入的Foundation替换为SwiftData,然后添加以下代码:
@Model
class Prospect {
var name: String
var emailAddress: String
var isContacted: Bool
}添加完上述代码后,在isContacted属性下方输入“in”,Xcode会自动补全初始化方法。
记住,SwiftData的@Model宏只能用于类,但这意味着我们可以在多个视图中共享该类的实例,从而确保所有视图都能自动保持数据同步。
现在我们已经有了要存储的数据模型,接下来可以告诉SwiftData为其创建一个模型容器(model container)。具体操作是打开HotProspectsApp.swift文件,导入SwiftData,然后添加modelContainer(for:)修饰符,代码如下:
WindowGroup {
ContentView()
}
.modelContainer(for: Prospect.self)这行代码不仅为Prospect类创建了存储,还会在应用的所有SwiftUI视图中注入一个共享的SwiftData模型上下文,只需一行代码就能完成这些操作。
我们希望所有ProspectsView实例都能共享这些模型数据,这样它们操作的就是同一组底层数据。要实现这一点,需要添加两个属性:一个用于访问刚才创建的模型上下文,另一个用于执行Prospect对象的查询。
现在打开ProspectsView.swift文件,导入SwiftData,然后在ProspectsView结构体中添加以下两个新属性:
@Query(sort: \Prospect.name) var prospects: [Prospect]
@Environment(\.modelContext) var modelContext提示: 如果打算使用Xcode的预览功能,请在预览代码中添加modelContainer(for: Prospect.self)。
这就是全部操作了——我认为SwiftData已经无法再简化这个过程了。
很快我们会添加通过扫描二维码来添加联系人的代码,但目前,我们先添加一个导航栏按钮,用于添加测试数据并在屏幕上显示。
将ProspectsView的body属性修改为以下内容:
NavigationStack {
Text("联系人数量:\(prospects.count)")
.navigationTitle(title)
.toolbar {
Button("扫描", systemImage: "qrcode.viewfinder") {
let prospect = Prospect(name: "保罗·哈德森", emailAddress: "paul@hackingwithswift.com", isContacted: false)
modelContext.insert(prospect)
}
}
}现在,在标签视图的前三个视图中,你会看到一个“扫描”按钮,点击该按钮会同时向三个视图中添加一个联系人——无论点击哪个视图中的按钮,你都会看到联系人数量增加。
动态过滤SwiftData查询结果
作者:Paul Hudson 2024年2月1日
我们的基础SwiftData查询代码如下:
@Query(sort: \Prospect.name) var prospects: [Prospect]默认情况下,该查询会加载所有Prospect模型对象,并按名称排序。对于“所有人”标签来说,这样的查询足够了,但对于另外两个标签则不够。
在我们的应用中,三个ProspectsView实例的区别仅在于从标签视图传入的FilterType属性值。我们已经利用这个属性来设置每个视图的标题,现在还可以用它来过滤查询结果。
没错,我们已经有了一个默认的查询,但通过添加初始化方法,我们可以在设置过滤条件时覆盖默认查询。
现在为ProspectsView添加以下初始化方法:
init(filter: FilterType) {
self.filter = filter
if filter != .none {
let showContactedOnly = filter == .contacted
_prospects = Query(filter: #Predicate {
$0.isContacted == showContactedOnly
}, sort: [SortDescriptor(\Prospect.name)])
}
}之前我们已经学习过如何手动创建查询,但有一行代码特别值得注意:
let showContactedOnly = filter == .contacted如果你对这行代码感到困惑,可以将其拆分为两部分来理解。首先是这个判断:
filter == .contacted如果filter的值等于.contacted,该判断会返回true,否则返回false。然后是这部分:
let showContactedOnly =这行代码会将filter == .contacted的结果赋值给一个名为showContactedOnly的新常量。所以,整行代码的意思是“如果过滤条件设置为.contacted,就将showContactedOnly设为true”。这样一来,我们的SwiftData谓词(predicate)就变得很简单了,只需将这个常量与isContacted直接进行比较即可。
添加完这个初始化方法后,我们就可以创建一个List来遍历查询得到的数组。我们将使用VStack来显示每个联系人的姓名和电子邮件地址,将ProspectsView中现有的文本视图替换为以下代码:
List(prospects) { prospect in
VStack(alignment: .leading) {
Text(prospect.name)
.font(.headline)
Text(prospect.emailAddress)
.foregroundStyle(.secondary)
}
}再次运行应用,你会发现应用的外观已经有了很大改善。