第84天 项目 16 第六部分
这是一个漫长的项目,期间有很多知识需要学习,但今天是代码编写的最后一天。正如美国教授安吉拉·达克沃斯所说:“热情常见,毅力难得”——要开始这个系列的第一天,需要热情,但如今你已经来到第84天,即将完成这个庞大的项目,显然你也拥有非凡的毅力。
这个项目已经运用了SwiftUI的一些重要特性,例如标签栏、滑动操作和环境;还用到了Swift的一些重要特性,比如导入外部包和Result类型;甚至涉及了iOS的一些重要特性,如Core Image和使用相机扫描二维码。今天我们要为这个项目“锦上添花”,即添加上下文菜单,并使用UserNotification框架显示通知。
优秀的应用就是这样的:它们借助各种语言和系统特性,打造出超越SwiftUI自身能力的出色用户体验。诚然,SwiftUI是构建应用的绝佳方式,但这仅仅是开始——iOS的能力远不止于此,虽然听起来像是陈词滥调,但你能创造出什么,唯一的限制就是你的想象力。
今天你需要完成两个任务:为二维码添加上下文菜单,然后使用UserNotification框架发送本地通知并在锁屏界面显示。
- 为图片添加上下文菜单
- 在锁屏界面显示通知
又一个应用完成了——别忘了和其他人分享你的进展!
为图片添加上下文菜单
作者:Paul Hudson 2024年2月11日
我们已经编写了根据用户姓名和电子邮件地址动态生成二维码的代码,但只需再添加一点代码,就能让用户将该二维码分享到应用外部。这是ShareLink派上用场的又一个例子,不过这次我们会把它放在上下文菜单中。
首先打开MeView.swift文件,为二维码图片添加contextMenu()修饰符,代码如下:
Image(uiImage: generateQRCode(from: "\(name)\n\(emailAddress)"))
.interpolation(.none)
.resizable()
.scaledToFit()
.frame(width: 200, height: 200)
.contextMenu {
let image = generateQRCode(from: "\(name)\n\(emailAddress)")
ShareLink(item: Image(uiImage: image), preview: SharePreview("My QR Code", image: Image(uiImage: image)))
}如你所见,我们需要将二维码的UIImage转换为SwiftUI的Image视图,然后才能将其传递给系统的分享面板。
我们可以通过缓存生成的二维码来减少一些工作量,但更重要的是,这样做后我们就不必每次都传入姓名和电子邮件地址——重复填写这些数据意味着将来如果修改了其中一处,就必须修改另一处。
要实现这一修改,首先添加一个新的@State属性来存储生成的二维码:
@State private var qrCode = UIImage()然后修改generateQRCode()方法,让它在返回新生成的二维码之前,先将其悄悄存储到我们的缓存中:
if let cgimg = context.createCGImage(outputImage, from: outputImage.extent) {
qrCode = UIImage(cgImage: cgimg)
return qrCode
}现在,上下文菜单按钮就可以使用缓存的二维码了:
.contextMenu {
ShareLink(item: Image(uiImage: qrCode), preview: SharePreview("My QR Code", image: Image(uiImage: qrCode)))
}在你亲自尝试上下文菜单之前,请确保为项目添加与Instafilter项目相同的配置选项——你需要在项目的配置选项中添加一条权限请求字符串。
如果你已经忘记了操作步骤,以下是所需的步骤:
- 打开目标设置
- 选择“Info”标签页
- 右键点击现有选项
- 选择“添加行”
- 为键名选择“Privacy - Photo Library Additions Usage Description”(隐私 - 照片库添加使用说明)
- 在值的位置输入“We want to save your QR code.”(我们希望保存你的二维码。)
现在运行应用,你可能会发现事情并没有按预期工作。实际上,在Xcode中,你可能会在generateQRCode()方法中看到一条紫色警告线:“Modifying state during view update, this will cause undefined behavior.”(在视图更新期间修改状态,这将导致未定义行为。)
这意味着当前视图体调用generateQRCode()来创建用于附加上下文菜单的可分享图片,但调用该方法现在会将值保存到我们用@State标记的qrCode属性中,而这又会导致视图体重新调用——形成了循环,因此SwiftUI会退出并标记一个严重警告。
要解决这个问题,我们需要让Image视图使用缓存的二维码,代码如下:
Image(uiImage: qrCode)然后结合使用onAppear()和onChange(),确保在视图首次显示时以及姓名或电子邮件地址发生变化时更新二维码。
这意味着需要创建一个新方法,在一个地方统一更新二维码:
func updateCode() {
qrCode = generateQRCode(from: "\(name)\n\(emailAddress)")
}然后在navigationTitle()下方附加一些额外的修饰符:
.onAppear(perform: updateCode)
.onChange(of: name, updateCode)
.onChange(of: emailAddress, updateCode)提示: 既然updateCode()会直接更新qrCode的值,我们可以恢复到generateQRCode()的早期版本,该版本只需返回新值即可:
if let cgimg = context.createCGImage(outputImage, from: outputImage.extent) {
return UIImage(cgImage: cgimg)
}现在这一步就完成了,而且实现得很完善——你应该可以运行应用,切换到“我的”(Me)标签页,然后长按二维码调出新的上下文菜单。
在锁屏界面显示通知
作者:Paul Hudson 2024年2月11日
作为我们应用的最后一部分功能,我们要在列表的滑动操作中添加另一个按钮,让用户可以选择设置提醒,以便联系特定的人。这将使用iOS的UserNotification框架创建本地通知,并且我们会在现有的if判断中根据条件决定是否在滑动操作中显示该按钮——只有当用户尚未联系过该人时,才会显示这个按钮。
更有趣的是我们如何安排本地通知的发送时间。记住,第一次尝试发送通知时,我们需要使用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)
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
center.add(request)
}
// 后续代码待添加
}这段代码将为当前联系人创建通知的所有代码都放入了一个闭包中,我们可以在需要时调用该闭包。注意,我使用了UNCalendarNotificationTrigger作为触发器,它允许我们指定一个自定义的DateComponents实例。我将其小时组件设置为9,这意味着通知将在下次上午9点触发。
提示: 为了测试方便,建议你注释掉上述触发器代码,并用以下代码替换,这样通知会在5秒后显示:
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)在该方法的第二部分,我们将同时使用getNotificationSettings()和requestAuthorization(),以确保只在允许的情况下安排发送通知。这里会用到我们上面定义的addRequest闭包,因为无论是已经拥有权限,还是请求后获得了权限,都可以使用相同的代码来安排发送通知。
将“// 后续代码待添加”这行注释替换为以下代码:
center.getNotificationSettings { settings in
if settings.authorizationStatus == .authorized {
addRequest()
} else {
center.requestAuthorization(options: [.alert, .badge, .sound]) { success, error in
if success {
addRequest()
} else if let error {
print(error.localizedDescription)
}
}
}
}安排为特定联系人发送通知所需的代码已经全部完成,剩下的就是在滑动操作中添加一个额外的按钮——在“Mark Contacted”(标记为已联系)按钮下方添加以下代码:
Button("Remind Me", systemImage: "bell") {
addNotification(for: prospect)
}
.tint(.orange)到这里,当前步骤就完成了,我们的项目也全部完成了——现在运行应用,你应该可以添加新的联系人,然后长按联系人条目,选择将其标记为已联系,或者设置联系提醒。
做得好!