「Apple Watch 应用开发系列」Apple Watch 上的推送通知
本地通知和远端通知是通知用户信息的好方法。如果我们的 watchOS App 有支持通知的 iOS App,则默认情况下,我们的 Apple Watch 会在适当的时候显示通知。不过,还可以做进一步的优化。
通知是一个大话题,本节将只关注我们在使用 watchOS 时需要注意的一些差异。你可以在另一篇文章 【iOS】朴实 Push 普识——了解 Push Notifications 全貌 中了解有关推送通知的更多信息。
接收通知的最佳设备
Apple 会尝试确定接收通知的最佳目标设备。如果我们只有 Apple Watch,通知就会出现在那里。但是如果我们使用手表和其他设备,则目标不仅取决于通知的类型,还取决于其来源。
我们会注意到图中的两个位置,Apple 询问是否将通知直接发送到手表。在 watchOS 6 及更高版本中,Apple Watch 是 remote 和 background 通知的有效目标。 Apple Watch extension 在注册 remote 通知时会收到一个唯一的 device token,就像在 iOS 中一样。
Short look
当 Apple Watch 收到通知时,它会通过振动通知用户。如果用户通过抬起手腕来查看通知,Apple Watch 会显示一个缩略版本,称为 Short look。如果用户查看通知的时间超过一秒,Apple Watch 将提供更详细的版本 Long look。
Short look 通知是展示给用户的快速摘要。简短的外观以预定义的布局显示应用程序的图标和名称,以及可选的通知标题。可选的通知标题是关于通知的简短说明,例如“新账单”、“提醒”或“分数警报”,并添加到 alert 键的值中。这让用户可以决定是否接着使用 Long look。
Long look
Long look 是一个可以自定义的滚动界面,带有默认的静态界面,或可选的动态创建的界面。与 Short look 界面不同,Long look 提供了更多的定制能力。
窗扇(Sash)是顶部水平的 Bar。默认情况下它是半透明的,但我们可以将其设置为任何颜色和不透明度值。我们可以通过实现 SwiftUI 视图来自定义内容区域,稍后我们将了解它。
虽然我们可以实现多个 UNNotificationAction 选项,但多项需要用户进行滚动操作,会有糟糕的用户体验。系统提供的 Dismiss 按钮始终位于界面底部。点击关闭会隐藏通知而不通知 Apple Watch App。
使用模拟器测试通知
构建 Pawsome 项目
新建项目,将其命名为 Pawsome,由于 Xcode 14 的更新,这里我们无法再选择 Notification Scene:
developer.apple.com/documentati…
Xcode 14 包含一个用于 watchOS 应用程序的默认模板,它将 WatchKit 应用程序和 WatchKit 应用程序扩展目标组合到一个 Watch 应用程序目标中,从而简化了代码、资产和本地化管理。
WatchKit 故事板在 watchOS 7.0 及更高版本中已弃用。请迁移到 SwiftUI 和 SwiftUI 生命周期。
为应用添加图标、图片资源:
修改 ContenView 代码,尝试展示图片资源,运行项目,你会看到猫咪列表展示:
struct ContentView: View {
var body: some View {
List(1..<21) { i in
Image("cat\(i)")
.resizable()
.scaledToFit()
}
.listStyle(CarouselListStyle())
}
}
复制代码
添加 LocalNotifications
新增 Notifications Scheme
我们新增一个 PushNotificationPayload.apns 文件:
{
"aps": {
"alert": {
"body": "Tap me to see an adorable kitty cat.",
"title": "Giggle Time!",
},
"category": "myCategory"
}
}
复制代码
我们接着新增 Scheme “Pawsome WatchKit App (Notification)”:
编辑 Schema,在 Run Tab 进行调整,切换至我们新增的 Scheme 并运行:
我们会看到出现了权限申请、允许后出现 Short Look通知,点击后出现 Long Look 通知,再次点击进入 App:
使用 WKUserNotificationHostingController 绑定 SwiftUI View
如果我们想自定义用户点击通知后的 App 的行为,该如何做呢?
首先,我们创建一个 NotificationView.swift,暂时无需做额外的调整:
import SwiftUI
struct NotificationView: View {
var body: some View {
Text("Hello, World!")
}
}
struct NotificationView_Previews: PreviewProvider {
static var previews: some View {
NotificationView()
}
}
复制代码
接着创建 NotificationController.swift,并将内容替换为:
import WatchKit
import SwiftUI
import UserNotifications
class NotificationController: WKUserNotificationHostingController<NotificationView> {
override var body: NotificationView {
return NotificationView()
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
override func didReceive(_ notification: UNNotification) {
// This method is called when a notification needs to be presented.
// Implement it if you use a dynamic notification interface.
// Populate your dynamic notification interface as quickly as possible.
}
}
复制代码
回到 PawsomeApp.swift,将内容替换为:
import SwiftUI
@main
struct PawsomeApp: App {
@SceneBuilder var body: some Scene {
WindowGroup {
NavigationView {
ContentView()
}
}
WKNotificationScene(
controller: NotificationController.self,
category: "myCategory"
)
}
}
复制代码
调用 WKNotificationScene 是告诉 Apple Watch 为有效负载中标识的每个类别显示什么视图。
重新构建项目,在出现 Short Look 后,点击通知,将会展示我们之前添加的 NotificationView.swift:
为通知添加 UNNotificationAction
新建 LocalNotifications.swift 文件,并添加代码:
import Foundation
import UserNotifications
final class LocalNotifications: NSObject {
static let categoryIdentifier = "Pawsome"
private let actionIdentifier = "viewCatsAction"
}
复制代码
categoryIdentifier、actionIdentifier 将在后续使用。
在 LocalNotifications 中添加代码:
override init() {
super.init()
Task {
do {
try await self.register()
try await self.schedule()
} catch {
print("⌚️ local notification: \(error.localizedDescription)")
}
}
}
func register() async throws {
}
func schedule() async throws {
}
复制代码
这里我们重写了 init 方法,并使用 **async **语法尝试 register、schedule 本地通知。
在 resign 方法中添加代码:
let current = UNUserNotificationCenter.current()
try await current.requestAuthorization(options: [.alert, .sound])
current.removeAllPendingNotificationRequests()
let action = UNNotificationAction(
identifier: self.actionIdentifier,
title: "More Cats!",
options: .foreground)
let category = UNNotificationCategory(
identifier: Self.categoryIdentifier,
actions: [action],
intentIdentifiers: [])
current.setNotificationCategories([category])
current.delegate = self
复制代码
我们请求了通知权限,然后使用刚刚的 categoryIdentifier 、actionIdentifier 构造了一个 UNNotificationAction 和 UNNotificationCategory,并将其设置为 NotificationCategory,并将自己设置为代理。
在代码最后补充代码,实现 UNUserNotificationCenterDelegate:
extension LocalNotifications: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
return [.list, .sound]
}
}
复制代码
这里是如何处理 App 行时到达的通知。
继续补充 schedule 方法中的代码:
let current = UNUserNotificationCenter.current()
let settings = await current.notificationSettings()
guard settings.alertSetting == .enabled else { return }
let content = UNMutableNotificationContent()
content.title = "Pawsome"
content.subtitle = "Guess what time it is"
content.body = "Pawsome time!"
content.categoryIdentifier = Self.categoryIdentifier
let components = DateComponents(minute: 30)
let trigger = UNCalendarNotificationTrigger(
dateMatching: components,
repeats: true)
let request = UNNotificationRequest(
identifier: UUID().uuidString,
content: content,
trigger: trigger)
try await current.add(request)
复制代码
判断权限后,我们创建了一个 UNMutableNotificationContent 并进行配置,构造一个 UNCalendarNotificationTrigger ,在每日凌晨 0 点 30 触发了。进而构造一个 UNNotificationRequest,并添加请求。
回到 PawsomeApp.swift,继续修改文件:
import SwiftUI
@main
struct PawsomeApp: App {
private let local = LocalNotifications()
@SceneBuilder var body: some Scene {
WindowGroup {
NavigationView {
ContentView()
}
}
WKNotificationScene(
controller: NotificationController.self,
category: "myCategory"
)
WKNotificationScene(
controller: NotificationController.self,
category: LocalNotifications.categoryIdentifier
)
}
}
复制代码
我们添加了 LocalNotifications 实例,同时添加了新的 category,并将 PushNotificationPayload.apns 中的 category 的值改为 Pawsome,构建项目:
现在,我们新增的 Action 展示出来了。
自定义 Long Look 样式
仔细看看 NotificationController.swift,我们会看到 body 返回了一个 NotificationView 的实例。 Controller 是我们接收和解析通知的地方。我们可以在视图中使用 Controller 收集的数据。
切换到 NotificationView.swift 以使通知以你想要的方式显示。 将默认文件的全部内容替换为:
import SwiftUI
struct NotificationView: View {
let message: String
let image: Image
var body: some View {
ScrollView {
Text(message)
.font(.headline)
image
.resizable()
.scaledToFit()
}
}
}
struct NotificationView_Previews: PreviewProvider {
static var previews: some View {
NotificationView(
message: "QWQ",
image: Image("cat\(Int.random(in: 1...20))")
)
}
}
复制代码
我们需要为要显示的视图提供消息和图像。然后只是在 ScrollView 中显示这两个属性。返回 NotificationController.swift 并将类的内容替换为:
class NotificationController: WKUserNotificationHostingController<NotificationView> {
var image: Image!
var message: String!
override var body: NotificationView {
return NotificationView(message: message, image: image)
}
override func didReceive(_ notification: UNNotification) {
let content = notification.request.content
message = content.body
let num = Int.random(in: 1...20)
image = Image("cat\(num)")
}
override func willActivate() {
super.willActivate()
}
override func didDeactivate() {
super.didDeactivate()
}
}
复制代码
我们存储标题和图像,然后为要显示的视图调用初始化程序,并传入适当的参数。再次构建并运行:
这次,我们的小猫咪出现啦~在这里,我们也可以完全不使用随机视图,而是修改为由 apns 指定,修改 PushNotificationPayload.apns:
{
"aps": {
"alert": {
"body": "Tap me to see an adorable kitty cat.",
"title": "Giggle Time!",
},
"category": "Pawsome"
},
"imageNumber": 5
}
复制代码
修改 NotificationController.swift 读取 imageNumber 字段:
override func didReceive(_ notification: UNNotification) {
let content = notification.request.content
message = content.body
let num = content.userInfo["imageNumber"] as! Int
image = Image("cat\(num)")
}
复制代码
交互式通知
点击显示详细信息。 是不是发生了什么意想不到的事情?默认情况下,推送通知不是交互式的。
将以下行添加到 NotificationController:
override class var isInteractive: Bool { true }
复制代码
再次构建并运行。 这一次,当你点击喵喵图片,将不会有任何相应,不会跳转到 App:
WKUserNotificationHostingController<T> 的 isInteractive 类型属性指定通知是否应接受用户输入。 默认值为 false,这意味着你只能与按钮交互。 通过将值更改为 true,你告诉 watchOS 通知应该接受用户输入。
这里我们并没有添加事件,所以没有任何相应。我们可以为其添加所需要的事件。
虽然我们解决了一个问题,但引入了另一个问题。 点击不再打开应用程序。 如果用户点击应用程序图标或窗扇上的任何位置,应用程序仍将打开。
自定义窗扇样式
编辑 NotificationController.swift 并在类的末尾添加以下行:
override class var sashColor: Color? { Color.green }
override class var titleColor: Color? { Color.purple }
override class var subtitleColor: Color? { Color.orange }
override class var wantsSashBlur: Bool { true }
复制代码
修改模拟器 watchOS 版本为 9.0 以前,再次构建并运行。 你会注意到颜色已经变成了可怕的东西:
请记住,watchOS 9.0 及以后,该功能失效。
Remote push notifications 基础
大多数应用程序使用远程推送通知而不是本地通知。在 iOS 中,我们必须创建一个扩展来修改传入的有效负载,如果我们想要一个自定义界面,则还必须创建另一个扩展。更糟糕的是,除非从 iOS 15 开始,否则我们无法使用 SwiftUI 进行自定义界面。然而在 watchOS 中,推送通知的工作方式与本地通知完全一样!我们学到的关于使用 WKUserNotificationHostingController 解析有效负载并返回自定义 SwiftUI 视图的所有内容都是一样的。
获取令牌
假设我们已经创建了推送通知令牌,这是我们从 Apple Developer 门户下载的以 p8 扩展名结尾的文件。这样,我们可以通过通知工具进行推送的发送。
创建 WKExtensionDelegate
在 iOS 中,我们使用 AppDelegate 注册推送通知。 watchOS 上不存在该类。 我们会使用 WKExtensionDelegate。 创建一个名为 ExtensionDelegate.swift 的新文件并粘贴:
import WatchKit
import UserNotifications
final class ExtensionDelegate: NSObject, WKExtensionDelegate {
func applicationDidFinishLaunching() {
Task {
do {
let success = try await UNUserNotificationCenter
.current()
.requestAuthorization(options: [.badge, .sound, .alert])
guard success else { return }
// 4
await MainActor.run {
WKExtension.shared().registerForRemoteNotifications()
}
} catch {
print(error.localizedDescription)
}
}
}
func didRegisterForRemoteNotifications(withDeviceToken deviceToken: Data) {
print(deviceToken.reduce("") { $0 + String(format: "%02x", $1) })
}
}
复制代码
我们声明一个实现 WKExtensionDelegate 的类。 由于该协议基于 NSObjectProtocol,因此你还需要从 NSObject 派生。就像在 iOS 中一样,只要注册发生,我们就可以获取 deviceToken。 在非实例的 App 中会存储和使用令牌,而不只是打印它。
我们使用 applicationDidFinishLaunching 来请求使用推送通知的权限。如果授予权限,则使用 WKExtension 单例注册推送通知,如果成功则调用 didRegisterForRemoteNotifications(withDeviceToken:)。
要告诉 watchOS 它应该使用我们的 ExtensionDelegate,请将以下代码添加到 PawsomeApp.swift 中 PawsomeApp 结构的顶部:
@WKExtensionDelegateAdaptor(ExtensionDelegate.self) private var extensionDelegate
复制代码
使用远程推送
回到这部分开头,watchOS 上的远程推送通知和本地通知没什么两样!欢迎参考另一篇文章 【iOS】朴实 Push 普识——了解 Push Notifications 全貌 了解有关远程推送通知的更多信息。
现在我们知道了在 watchOS 上显示自定义通知的基础知识,我们还可以从这里做更多的事情,包括处理用户从通知中选择的操作。也欢迎参阅 Apple 的用户通知 (apple.co/3hFtwKR) 文档。
有关创建 Scheme 和 JSON 有效负载以及直接在手表上进行测试的更多详细信息,请参阅 Apple 开发人员文档中的测试自定义通知 (apple.co/3tTDE5F)。
附件
- Pawsome 项目的所有文件请参考:github.com/LLLLLayer/A…