Highlight
watchOS 提供前台刷新、Background App Refresh、background URLSession 和 complication push 四种机会更新 ClockKit timeline;后台刷新通常最多每小时 4 次,complication push 每只 Watch 每天最多 50 次,App 需要按数据来源控制更新节奏。
核心内容
Apple Watch 的 complication 一直在表盘上。用户抬腕看到的是 App 的状态,不一定会先打开 App。watchOS 因此会给带 complication 的 App 一些特殊机会:App 可能被系统保留,必要时也会被重新启动来更新表盘内容(00:43)。
这场演讲的范围很窄:已经有 complication 后,如何让它在用户没有打开 App 时继续显示新数据。示例 App 是一个独立 Watch App,表盘上有三类信息:从 HealthKit 取活动数据、从服务器取天气和风速、接收朋友发来的鼓励消息(02:13)。
前台更新最直接。用户打开 App、修改选择、或者 App 在前台收到新数据时,代码调用 CLKComplicationServer.sharedInstance().reloadTimeline(for:),ClockKit 随后请求当前 timeline entry。这里要选择性更新,只更新真正需要变化的 complication(03:11)。
后台更新要按数据来源选机制。Background App Refresh 适合访问 Watch 上的 API 和本地数据,例如 HealthKit;background URLSession 适合从服务器下载数据;complication push 适合事件驱动的数据,例如朋友发来一条新鼓励。三种机制都能在 App 不活跃时唤醒 App,但每一种都有频率、时间和完成任务的约束(01:33)。
详细内容
前台用 reloadTimeline 触发当前 entry 重建
(03:32)updateActiveComplications() 取得当前活跃的 complications,然后逐个要求 ClockKit 重新加载 timeline。
class ExtensionDelegate: NSObject, WKExtensionDelegate {
func updateActiveComplications() {
let complicationServer = CLKComplicationServer.sharedInstance()
if let activeComplications = complicationServer.activeComplications {
for complication in activeComplications {
complicationServer.reloadTimeline(for: complication)
}
}
}
}
关键点:
CLKComplicationServer.sharedInstance()是更新 ClockKit timeline 的入口。activeComplications只包含当前表盘上真正启用的 complication。- 示例代码更新所有活跃 complications;演讲提醒真实 App 应该更细,只刷新需要更新的项。
- 每次
reloadTimeline(for:)之后,ClockKit 会再向 data source 请求当前 entry。
(04:26)ClockKit 请求当前 entry 时,App 根据 complication family 创建对应 template,并通过 handler 交回 CLKComplicationTimelineEntry。
class ComplicationController: NSObject, CLKComplicationDataSource {
func getCurrentTimelineEntry(for complication: CLKComplication,
withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void) {
switch (complication.family) {
case .modularSmall:
let template = CLKComplicationTemplateModularLargeTallBody.init(
headerTextProvider: headerTextProvider,
bodyTextProvider: bodyTextProvider)
entry = CLKComplicationTimelineEntry(date: Date(),
complicationTemplate: template)
}
handler(entry)
}
}
关键点:
complication.family决定要返回哪一种 template。- template 由 text providers 等内容提供者填充。
CLKComplicationTimelineEntry(date:complicationTemplate:)把生效时间和显示模板绑定到一起。handler(entry)是必须完成的回调,ClockKit 通过它取得要显示的内容。
Background App Refresh 适合本地数据和 Watch API
(05:20)示例 App 用 Background App Refresh 拉取 HealthKit 活动数据。演讲给出的频率上限是每小时最多四次,实际次数还会受其他进程和电量影响。
private func scheduleBAR(_ first: Bool) {
let now = Date()
let scheduledDate = now.addingTimeInterval(first ? 60 : 15*60)
let info: NSDictionary = ["submissionDate": now]
let wkExt = WKExtension.shared()
wkExt.scheduleBackgroundRefresh(withPreferredDate: scheduledDate, userInfo: info)
{ (error: Error?) in
if (error != nil) {
print("background refresh could not be scheduled \(error.debugDescription)")
}
}
}
关键点:
first ? 60 : 15*60对应首次尽快调度,后续按 15 分钟节奏请求。userInfo可以带上调度时的上下文,示例用submissionDate计算系统实际唤醒延迟。scheduleBackgroundRefresh(withPreferredDate:userInfo:)的时间是偏好时间,系统只保证在这个时间之后选择合适时机唤醒。- completion handler 在主线程异步调用,调度失败要处理错误。
(08:47)如果后台工作是异步的,必须等数据提供者完成后再更新 complication、安排下一次刷新、标记任务完成。
class ExtensionDelegate: NSObject, WKExtensionDelegate {
var healthDataProvider: HealthDataProvider
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
for task in backgroundTasks {
switch task {
case let backgroundTask as WKApplicationRefreshBackgroundTask:
healthDataProvider.refresh() { (update: Bool) -> Void in
if update {
self.updateActiveComplications()
}
self.scheduleBAR(first: false)
backgroundTask.setTaskCompletedWithSnapshot(false)
}
关键点:
WKApplicationRefreshBackgroundTask是 Background App Refresh 到达时的任务类型。healthDataProvider.refresh()负责异步读取 HealthKit 数据。- 只有
update为 true 时才调用updateActiveComplications(),避免没有数据变化时浪费刷新机会。 scheduleBAR(first: false)在当前任务完成前安排下一次请求,因为同一时间只有一个请求处于 outstanding 状态。setTaskCompletedWithSnapshot(false)必须在所有工作完成后调用;演讲指出忘记标记完成是超过 15 秒总时限的常见原因。
Background App Refresh 不能做网络请求。演讲明确说 URLSession 是例外,尝试使用会失败。它还给出两个时间限制:最多 4 秒 active CPU time,最多 15 秒 total time(09:22)。
background URLSession 适合服务器数据
(11:35)天气数据来自服务器,所以示例 App 改用 background URLSession。关键设置是 background configuration、非 discretionary、以及 sessionSendsLaunchEvents = true。
class WeatherDataProvider: NSObject, URLSessionDownloadDelegate {
private lazy var backgroundURLSession: URLSession = {
let config = URLSessionConfiguration.background(withIdentifier: "BackgroundWeather")
config.isDiscretionary = false
config.sessionSendsLaunchEvents = true
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()
关键点:
URLSessionConfiguration.background(withIdentifier:)创建后台下载会话。isDiscretionary = false表示请求不交给系统自由推迟。sessionSendsLaunchEvents = true允许下载事件到达时在后台唤醒 App。delegateQueue: nil让 URLSession delegate 回调进入后台 serial queue。
(12:02)下载任务通过 earliestBeginDate 指定最早开始时间,并设置预期发送/接收字节数。
func schedule(_ first: Bool) {
if backgroundTask == nil {
if let url = self.currentWeatherURLForLocation(delegate.currentLocationCoordinate)
{
let bgTask = backgroundURLSession.downloadTask(with: url)
bgTask.earliestBeginDate = Date().addingTimeInterval(first ? 60 : 15*60)
bgTask.countOfBytesClientExpectsToSend = 200
bgTask.countOfBytesClientExpectsToReceive = 1024
bgTask.resume()
backgroundTask = bgTask
}
}
}
关键点:
backgroundTask == nil保证这个示例里同一时间只保留一个天气下载任务。- URL 来自最新缓存位置,演讲场景是按地点获取风速天气。
earliestBeginDate复用首次 60 秒、后续 15 分钟的调度策略。countOfBytesClientExpectsToSend和countOfBytesClientExpectsToReceive帮系统了解传输规模。resume()之后,下载可以独立于 App 运行;完成后系统会通过WKURLSessionRefreshBackgroundTask唤醒 App。
(14:08)下载完成后,delegate 从文件 URL 读取数据并处理 JSON。之后 didCompleteWithError 在主线程调用保存的 completion handler。
class WeatherDataProvider: NSObject, URLSessionDownloadDelegate {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL) {
if location.isFileURL {
do {
let jsonData = try Data(contentsOf: location)
if let kiteFlyingWeather = KiteFlyingWeather(jsonData) {
// Process weather data here.
}
} catch let error as NSError {
print("could not read data from \(location)")
}
}
}
关键点:
didFinishDownloadingTo收到的是下载到本地的文件位置。- 示例先确认
location.isFileURL,再读取Data(contentsOf:)。 KiteFlyingWeather(jsonData)是示例 App 的天气解析入口。- 任务完成后仍要等 URLSession delegate 事件交付完,再标记
WKURLSessionRefreshBackgroundTask完成。
background URLSession 通常也可以每小时发送和接收最多四个请求,实际次数受 Wi-Fi、蜂窝信号和电量影响。App 被激活时要重新 attach 到 session,这样才能收到可能待处理的 delegate callbacks(10:31)。
complication push 适合事件驱动数据
(16:16)如果数据来自服务器上的突发事件,push 比定时拉取更合适。示例 App 用 complication push 更新朋友发来的鼓励消息。
class PushNotificationProvider: NSObject, PKPushRegistryDelegate {
func startPushKit() -> Void {
let pushRegistry = PKPushRegistry(queue: .main)
pushRegistry.delegate = self
pushRegistry.desiredPushTypes = [.complication]
}
func pushRegistry(_ registry: PKPushRegistry,
didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
// Send credentials to server
}
func pushRegistry(_ registry: PKPushRegistry,
didReceiveIncomingPushWith payload: PKPushPayload,
for type: PKPushType, completion: @escaping () -> Void) {
// Process payload
delegate.updateActiveComplications()
completion()
}
}
关键点:
PKPushRegistry(queue: .main)指定 PushKit 回调使用主队列。desiredPushTypes = [.complication]对应 complication push 类型。didUpdate pushCredentials返回的凭据要上传给服务器。- 收到 push 后先处理 payload,再调用
updateActiveComplications()。 completion()必须在 payload 处理完成后调用。
服务器侧也有硬性要求:App identifier 必须以 .watchkitapp.complication 结尾,并用它创建 Apple Push Notification service SSL Certificate。WatchKit extension 还需要 remote notification background mode 和 push notifications capability(16:51)。
每只 Watch 每天最多收到 50 个 complication pushes。第 51 个以及之后的通知会被忽略,所以服务器要按用户和设备做节流。push payload 按后台 push 的方式组织,在 aps 字典里提供 content-available(18:34)。
核心启发
- 活动进度 complication:做什么:在表盘显示当天活动状态。为什么值得做:session 示例用 Background App Refresh 读取 HealthKit,并且后台任务可以每小时最多运行四次。怎么开始:把 HealthKit 查询放进
HealthDataProvider.refresh(),只有数据变化时再调用updateActiveComplications()。 - 风速天气 complication:做什么:根据最新缓存位置显示适合放风筝的风速和天气。为什么值得做:session 示例用 background URLSession 在 App 不活跃时下载服务器数据。怎么开始:创建
URLSessionConfiguration.background,设置sessionSendsLaunchEvents = true,下载完成后解析 JSON 并更新 timeline。 - 朋友鼓励 complication:做什么:在表盘显示朋友发来的最新鼓励。为什么值得做:session 说明事件驱动数据适合 complication push,push 可以按突发事件发送。怎么开始:注册
PKPushRegistry的.complication类型,把 push credentials 上传到服务器,并在收到 payload 后刷新相关 complication。 - 设置变更即时预览:做什么:用户在 Watch App 里改了地点、目标或显示项后,表盘立即显示新内容。为什么值得做:session 建议在 App 前台活跃、状态响应输入变化时更新 complications。怎么开始:在设置保存后取得
activeComplications,只对受影响的 complication 调用reloadTimeline(for:)。
关联 Session
- Create complications for Apple Watch — 先补 ClockKit complication 的 timeline、family、template 和 multiple complication 基础。
- Build complications in SwiftUI — 继续看 Graphic complications 如何使用 SwiftUI view,与本 session 的更新机制组合。
- Meet Watch Face Sharing — 学习如何分享包含 App complications 的 watch faces;这也是本 session 结尾点名推荐的内容。
- Create quick interactions with Shortcuts on watchOS — 了解从 complication 触发 watchOS Shortcut 的轻量交互入口。
评论
GitHub Issues · utterances