Hacking with iOS: SwiftUI Edition - 潜力客户名单项目(三)

时间:2022-07-25
本文章向大家介绍Hacking with iOS: SwiftUI Edition - 潜力客户名单项目(三),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

放弃字符串,然后使用封装和访问控制是使我们的代码更安全的简单方法,并且是构建更好软件的重要步骤。

使用 UserDefaults 保存和加载数据

该应用程序大多数情况下都可以运行,但是有一个致命缺陷:重新启动该应用程序时,我们添加的所有数据都会被清除掉,这在记住我们认识的人方面没有多大用处。我们可以通过使 Prospects 初始化程序能够从UserDefaults加载数据,然后在数据更改时将其写回来解决此问题。

这次,我们的数据以稍微容易些的格式存储:尽管Prospects类使用@Published属性包装器,但其中的people数组非常简单,仅通过添加协议就已经符合Codable。因此,我们可以通过进行三个小更改来实现目标的大部分方法:

  1. 更新Prospects初始化程序,以便在可能的情况下从UserDefaults加载其数据。
  2. save()方法添加到同一类中,然后将当前数据写入UserDefaults
  3. 在添加潜在客户或切换其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()来确保始终将数据保存。

第一个是在Prospectstoggle()方法中,因此将其修改为:

func toggle(_ prospect: Prospect) {
    objectWillChange.send()
    prospect.isContacted.toggle()
    save()
}

第二个是在ProspectsViewhandleScan(result:)方法中,我们在该方法中向列表添加新的潜在客户。找到这一行:

self.prospects.people.append(person)

并直接在下面添加:

self.prospects.save()

如果您现在运行该应用程序,您会发现即使重新启动该应用程序后,添加的任何联系人仍将保留在那里,因此我们可以轻松地在此处停止。但是,这次我想更进一步,解决其他两个问题:

  1. 我们必须在两个地方对键名“SavedData”进行硬编码,如果名称更改或需要在更多地方使用,将来可能再次引起问题。
  2. 必须在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