Hacking with iOS: SwiftUI Editio
- 放弃字符串,然后使用封装和访问控制是使我们的代码更安全的简单方法,并且是构建更好软件的重要步骤。
使用 UserDefaults 保存和加载数据
该应用程序大多数情况下都可以运行,但是有一个致命缺陷:重新启动该应用程序时,我们添加的所有数据都会被清除掉,这在记住我们认识的人方面没有多大用处。我们可以通过使 Prospects 初始化程序能够从UserDefaults
加载数据,然后在数据更改时将其写回来解决此问题。
这次,我们的数据以稍微容易些的格式存储:尽管Prospects
类使用@Published
属性包装器,但其中的people
数组非常简单,仅通过添加协议就已经符合Codable
。因此,我们可以通过进行三个小更改来实现目标的大部分方法:
- 更新
Prospects
初始化程序,以便在可能的情况下从UserDefaults
加载其数据。 - 将
save()
方法添加到同一类中,然后将当前数据写入UserDefaults
。 - 在添加潜在客户或切换其
isContacted
属性时调用save()
。
我们之前已经看过代码可以完成所有这些工作,因此让我们开始吧。我们已经为Prospects
提供了一个简单的初始化程序,因此我们可以将其更新为使用UserDefaults
,如下所示:
init() {
if let data = UserDefaults.standard.data(forKey: "SavedData") {
if let decoded = try? JSONDecoder().decode([Prospect].self, from: data) {
self.people = decoded
return
}
}
self.people = []
}
至于save()
方法,这将做相反的事情——添加以下内容:
func save() {
if let encoded = try? JSONEncoder().encode(people) {
UserDefaults.standard.set(encoded, forKey: "SavedData")
}
}
我们的数据在两个地方进行了更改,因此我们都需要调用save()
来确保始终将数据保存。
第一个是在Prospects
的toggle()
方法中,因此将其修改为:
func toggle(_ prospect: Prospect) {
objectWillChange.send()
prospect.isContacted.toggle()
save()
}
第二个是在ProspectsView
的handleScan(result:)
方法中,我们在该方法中向列表添加新的潜在客户。找到这一行:
self.prospects.people.append(person)
并直接在下面添加:
self.prospects.save()
如果您现在运行该应用程序,您会发现即使重新启动该应用程序后,添加的任何联系人仍将保留在那里,因此我们可以轻松地在此处停止。但是,这次我想更进一步,解决其他两个问题:
- 我们必须在两个地方对键名“SavedData”进行硬编码,如果名称更改或需要在更多地方使用,将来可能再次引起问题。
- 必须在
ProspectsView
中调用save()
并不是一个好的设计,部分原因是我们的视图确实不应该知道其模型的内部工作原理,而且还因为如果我们有其他视图在处理数据,那么我们可能会忘记调用save()
那里。
为了解决第一个问题,我们应该在Prospects
上创建一个静态属性以包含我们的保存键,因此我们对UserDefaults
使用该属性而不是字符串。
将此添加到Prospects
类中:
static let saveKey = "SavedData"
然后,我们可以使用它而不是硬编码的字符串,首先通过修改初始化程序,如下所示:
if let data = UserDefaults.standard.data(forKey: Self.saveKey) {
保存方法同上修改,Self
在此为 Prospects
所以和写 Prospects.saveKey
是一样的意思
从长远来看,这种方法更安全——偶然编写“SaveKey”或“savedKey”太容易了,这样做会引入各种错误。
至于调用save()
的问题,这实际上是一个更深层次的问题:当我们编写诸如self.prospects.people.append(person)
之类的代码时,我们正在打破一种称为 封装 的软件工程原理。这是一个想法,我们应该限制一个类或结构体中可以读取和写入值的外部对象数量,并且提供读取(获取)和写入(设定)数据的方法。
实际上,这意味着我们无需编写self.prospects.people.append(person)
而是在Prospects
类上创建add()
方法,因此我们可以编写如下代码:self.prospects.add(person)
。结果将是相同的——我们的代码将一个人员添加到人员数组中——但是现在隐藏了实现。这意味着我们可以将数组切换到其他位置,而ProspectsView
不会中断,但这也意味着我们可以向add()
方法添加额外的功能。
因此,为了解决第二个问题,我们将在Prospects
中创建一个add()
方法,以便我们可以在内部触发save()
。立即添加:
func add(_ prospect: Prospect) {
people.append(prospect)
save()
}
更好的是,我们可以使用访问控制来停止对people
数组的外部写入,这意味着我们的视图必须使用add()
方法添加前景。这是通过将people
属性的定义更改为以下内容来完成的:
@Published private(set) var people: [Prospect]
现在,只有Prospects
内部的代码才调用save()
方法,我们也可以将其标记为私有的:
private func save() {
这有助于锁定我们的代码,以便我们不会因偶然而犯错误——编译器根本不允许这样做。实际上,如果您现在尝试构建代码,您将确切理解我的意思:ProspectsView
尝试追加到people
数组并调用save()
,这不再被允许。
要解决该错误并让我们的代码再次干净地编译,请用以下代码替换这两行:
self.prospects.add(person)
放弃字符串,然后使用封装和访问控制是使我们的代码更安全的简单方法,并且是构建更好软件的重要步骤。
发送本地通知到锁屏界面
对于应用程序的最后一部分,我们将在上下文菜单中添加另一个按钮,以提醒用户选择联系特定人员。这将使用iOS的UserNotifications
框架创建本地通知,我们将通过简单的if
选中条件将其包含在上下文菜单中——SwiftUI足够聪明,如果测试通过,则可以添加上下文菜单按钮。
更有趣的是我们如何安排本地通知。请记住,第一次尝试时,我们需要使用requestAuthorization()
显式请求在锁定屏幕上显示通知的权限,但随后的时间我们也要小心,因为用户可以随时改变主意并禁用通知。
一种选择是,每当我们要发布通知时,都调用requestAuthorization()
,这确实很有效:第一次显示警报,而在所有其他情况下,它将根据先前的响应立即返回成功或失败。
但是,出于完成的目的,我想向您展示一个更强大的替代方案:我们可以请求当前的授权设置,并使用该设置来确定是否应该安排通知或请求许可。使用此方法而不是重复请求权限的帮助,是因为交还给我们的设置对象包含诸如alertSetting
之类的属性,用于检查我们是否可以显示警报——用户可能已对此进行了限制,因此我们可以要做的是在我们的图标上显示一个角标。
因此,我们将调用getNotificationSettings()
来了解当前是否允许通知。如果是,我们将显示一条通知。如果没有,我们将请求权限,如果成功返回,我们还将显示一条通知。我们无需重复代码来安排通知,而是将其放在可以在两种情况下均可调用的闭包中。
首先在 ProspectsView.swift 顶部附近添加此导入:
import UserNotifications
现在,将此方法添加到ProspectsView
结构体中:
func addNotification(for prospect: Prospect) {
let center = UNUserNotificationCenter.current()
let addRequest = {
let content = UNMutableNotificationContent()
content.title = "Contact \(prospect.name)"
content.subtitle = prospect.emailAddress
content.sound = UNNotificationSound.default
var dateComponents = DateComponents()
dateComponents.hour = 9
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: false)
// identifier可以用其他字符串替代,如果你想当通知还未发出你想取消,或者通知已经发出但是你想让他不再显示
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
center.add(request)
}
// more code to come
}
这会将所有用于为当前潜在客户创建通知的代码置于闭包中,我们可以在需要时调用它。请注意,我已将UNCalendarNotificationTrigger
用于触发器,该触发器使我们可以指定自定义DateComponents
实例。我将其小时部分设置为9,这意味着它将在下次上午9点触发。
提示:出于测试目的,建议您注释掉该触发代码,然后将其替换为以下代码,该代码从现在起五秒钟显示警报:
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
对于该方法的第二部分,我们将一起使用getNotificationSettings()
和requestAuthorization()
,以确保仅在允许时安排通知。这将使用我们上面定义的addRequest
闭包,因为如果我们已经拥有权限,或者如果我们询问并已被授予权限,则可以使用相同的代码。
替换 // more code to come
:
center.getNotificationSettings { settings in
if settings.authorizationStatus == .authorized {
addRequest()
} else {
center.requestAuthorization(options: [.alert, .badge, .sound]) { success, error in
if success {
addRequest()
} else {
print("Can't send")
}
}
}
}
这就是我们为特定潜在客户安排通知所需的全部代码,因此剩下的就是向我们的上下文菜单添加一个额外的按钮——将其添加到上一个按钮的下方:
if !prospect.isContacted {
Button("Remind Me") {
self.addNotification(for: prospect)
}
}
这样就完成了当前步骤,也完成了我们的项目——立即尝试运行它,您应该发现可以添加新的潜在客户,然后按住以将其标记为已联系,或者安排联系提醒。
译自
Saving and loading data with UserDefaults
Posting notifications to the lock screen