Highlight
watchOS 7 让一个 App 提供多个 Complication,并允许部分 Graphic template 使用 SwiftUI 视图,同时仍由 ClockKit timeline、family 和 template 驱动表盘内容。
核心内容
Apple Watch App 不能只等用户点开。表盘才是用户最常看的位置,Complication 把 App 的最新状态放到那里:下一班船、当前比分、倒计时、今日进度,都可以在抬腕时出现(00:23)。
ClockKit 的做法是先让 App 交出一条 timeline。每个 entry 有一个生效时间和一个 template;系统按时间自动切换 entry,不需要 App 一直运行。Session 用观鲸行程解释这个模型:行程本来有起止时间,但 Complication entry 只绑定一个时间,并且可以提前于真实开始时间出现,给用户留出赶船时间(01:33)。
这场 session 还把 watchOS 7 的两个变化放进同一条链路:Graphic Extra Large family 新增到 Extra Large 表盘;一个 App 可以提供多个 Complication。Whale Watch 示例里,同一个 App 同时提供不同观测站的目击信息、记录新目击的入口、以及整体季节数据(03:05、11:47)。
SwiftUI 在这里不是替代全部 ClockKit。演讲说的是:使用 CLKFullColorImageProvider 的 complication templates 有 SwiftUI view alternatives。开发者仍然需要提供 timeline、选择 family、返回 template,只是在部分 Graphic template 中可以把 SwiftUI view 放进去(11:15)。
详细内容
用 timeline 交出当前和未来状态
(04:54)CLKComplicationDataSource 只有一个必需方法:getCurrentTimelineEntry(for:withHandler:)。它回答“现在表盘应该显示什么”。
// CLKComplicationDataSource - Required
class ComplicationController: NSObject, CLKComplicationDataSource {
func getCurrentTimelineEntry(
for complication: CLKComplication,
withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void)
{
// Call the handler with the current timeline entry
handler(createTimelineEntry(forComplication: complication, date: Date()))
}
}
关键点:
ComplicationController遵守CLKComplicationDataSource,ClockKit 会通过它请求内容。complication表示表盘上的一个具体实例,后面会用它区分 family 和 identifier。handler接收一个CLKComplicationTimelineEntry?,entry 里包含生效时间和 template。createTimelineEntry(forComplication:date:)是 App 自己的封装,把当前时间转成表盘可显示的 entry。
(05:16)如果 App 可以预先提供未来数据,还要实现 timeline end date 和 after-date entries。ClockKit 会按 limit 分批请求,超出 limit 的 entry 会被丢弃。
// CLKComplicationDataSource - Timeline Support
extension ComplicationController {
func getTimelineEndDate(
for complication: CLKComplication,
withHandler handler: @escaping (Date?) -> Void)
{
handler(timeline(for: complication)?.endDate)
}
func getTimelineEntries(
for complication: CLKComplication,
after date: Date,
limit: Int,
withHandler handler: @escaping ([CLKComplicationTimelineEntry]?) -> Void)
{
handler(timeline(for: complication)?.entries(after: date, limit: limit))
}
}
关键点:
getTimelineEndDate告诉系统这条 timeline 能覆盖到什么时候。getTimelineEntries从系统已有的最后一个 entry 之后继续提供数据。date是系统已经拥有的最后一个 timeline entry 时间。limit是系统本次需要的最大数量,App 不应该一次塞入更多 entry。
当数据完全失效时,调用 CLKComplicationServer 的 reloadTimeline(for:)。如果已有 entry 仍有效,只是 App 能提供更多未来 entry,就调用 extendTimeline(for:)。系统只跟踪当前在用户表盘上的 Complication,这些实例从 activeComplications 或 data source 方法参数里取得,不要自己创建 CLKComplication 对象(06:00)。
用 provider 适配极小空间
(07:47)Complication 的空间很小,同一段文字要出现在不同 family 和 template 中。ClockKit 用 data providers 表达数据,再替开发者处理不同布局下的显示。
let longDate: Date = DateComponents(year: 2020, month: 9, day: 23).date ?? Date()
let units: NSCalendar.Unit = [.weekday, .month, .day]
let textProvider = CLKDateTextProvider(date: longDate, units: units)
关键点:
longDate是要显示的日期。units指定最长情况希望展示 weekday、month、day。CLKDateTextProvider会根据空间从完整日期逐步降级到更短文本。
(08:49)相对时间用 CLKRelativeDateTextProvider。演讲给出的例子是计时器风格,它会随当前时间自动更新,可以精确到秒,不需要 App 额外刷新字符串。
let timerStart: Date = …
let units: NSCalendar.Unit = [.hour, .minute, .second]
let textProvider = CLKRelativeDateTextProvider(date: timerStart, style: .timer, units: units)
关键点:
timerStart是相对时间计算的目标时间。style: .timer让 provider 按计时器形式展示。units控制展示小时、分钟、秒。- 自动更新时间由 ClockKit 处理,适合倒计时和剩余时间。
同一段里还提到 CLKTimeTextProvider、CLKTimeIntervalTextProvider、CLKSimpleTextProvider、CLKImageProvider、CLKFullColorImageProvider、CLKTimeIntervalGaugeProvider。这些 provider 的共同目标是让内容在不同表盘颜色、不同 family、不同空间大小下仍能被系统正确压缩和着色(09:01、10:42)。
用 descriptor 声明多个 Complication
(12:07)watchOS 7 新增 multiple complication support。相关方法是 getComplicationDescriptors(withHandler:) 和 handleSharedComplicationDescriptors(_:)。CLKComplicationDescriptor 包含唯一 identifier、编辑界面展示的 displayName、支持的 families,以及可选的 userInfo 或 userActivity。
// CLKComplicationDataSource - Multiple Complication Support
extension ComplicationController {
var descriptors : [CLKComplicationDescriptor] = []
var dataDict = Dictionary<AnyHashable, Any>()
for station in data.stations {
dataDict = [“name": station.name, “shortName": station.shortName]
descriptors.append(
CLKComplicationDescriptor(
identifier: station.name,
displayName: station.name,
supportedFamilies: CLKComplicationFamily.allCases,
userInfo: dataDict))
}
descriptors.append(
CLKComplicationDescriptor(
identifier: "LogSighting",
displayName: "Log Sighting",
supportedFamilies: CLKComplicationFamily.allCases))
descriptors.append(
CLKComplicationDescriptor(
identifier: "SeasonData",
displayName: "Season Data",
supportedFamilies: [.graphicRectangular]))
// Call the handler with the currently supported complication descriptors
handler(descriptors)
}
关键点:
- 每个 station 都变成一个 descriptor,identifier 和 displayName 使用 station 名称。
userInfo把name、shortName这类业务数据带到之后的 template 创建阶段。LogSighting是一个快速记录新目击的 Complication,支持所有 family。SeasonData只支持.graphicRectangular,因为整体季节数据需要较大的画布。- descriptor 列表变化时,调用
reloadComplicationDescriptors(),系统会重新请求支持列表。
如果用户当前表盘上还放着某个 App 已经不再支持的 Complication,系统仍会继续请求它的 timeline entries。Session 要求开发者尽量继续给出有用内容。另一个必须处理的是 CLKDefaultComplicationIdentifier:旧版 Complication、共享表盘丢失关联数据时,系统可能会用这个 identifier 请求内容(14:17、15:59)。
按 family 和 identifier 返回 template
(17:33)createTimelineEntry 先创建 template,再把 template 和日期包装成 CLKComplicationTimelineEntry。
func createTimelineEntry(
forComplication complication: CLKComplication,
date: Date) -> CLKComplicationTimelineEntry?
{
guard let template = createTemplate(forComplication: complication, date: date) else {
return nil
}
return CLKComplicationTimelineEntry(date: date, complicationTemplate: template)
}
关键点:
createTemplate决定具体显示样式。- template 创建失败时返回
nil,不给系统无效 entry。 - 成功时,
date决定 entry 何时开始显示。 complicationTemplate承载最终交给表盘的视觉内容。
(17:44)Whale Watch 示例按 (complication.family, complication.identifier) 分支。SeasonData 在 Graphic Rectangular 里返回 SwiftUI chart;LogSighting 在 Graphic Circular 里返回图片加文字;其他观测站根据 family 返回不同 template;无法处理时退回 default template。
func createTemplate(
forComplication complication: CLKComplication,
date: Date) -> CLKComplicationTemplate?
{
var station: Station? = nil
if let stationName = complication.userInfo?["name"] as? String {
station = data.stations.first(where: { $0.name == stationName })
}
let image = UIImage(named: "Spout-small")!
let spoutFullColorImageProvider = CLKFullColorImageProvider(fullColorImage: image)
let logSightingTextProvider = CLKSimpleTextProvider(
text: "Log Sighting",
shortText: "Log")
let defaultTemplate: (CLKComplicationFamily) -> CLKComplicationTemplate = { family -> CLKComplicationTemplate in
// Return a default complication template for the given family
}
switch (complication.family, complication.identifier) {
case (.graphicRectangular, "SeasonData"):
return CLKComplicationTemplateGraphicRectangularFullView(
ChartView(
seriesData: data.last7DaysSightings,
seriesColor: .turquoise)
case (.graphicCircular, "LogSighting"):
return CLKComplicationTemplateGraphicCircularStackImage(
line1ImageProvider: spoutFullColorImageProvider,
line2TextProvider: logSightingTextProvider)
case (.graphicCircular, _):
guard let station = station else { return defaultTemplate(.graphicCircular) }
return CLKComplicationTemplateGraphicCircularView(
SightingTypeView(station: station))
case (.graphicCorner, _):
guard let station = station else { return defaultTemplate(.graphicCorner) }
return CLKComplicationTemplateGraphicCornerTextImage(
textProvider: station.timeAndShortLocTextProvider,
imageProvider: station.whaleActivityFullColorProvider)
case (.graphicExtraLarge, _):
guard let station = station else { return defaultTemplate(.graphicExtraLarge) }
return CLKComplicationTemplateGraphicExtraLargeCircularStackText(
line1TextProvider: station.timeAndLocationTextProvider,
line2TextProvider: station.shortLocationTextProvider)
default:
return defaultTemplate(complication.family)
}
}
关键点:
complication.userInfo?["name"]读取 descriptor 带来的 station 名称。CLKFullColorImageProvider用于 Graphic family 的全彩图像。CLKSimpleTextProvider给LogSighting同时提供长文本和短文本。switch同时看 family 和 identifier,避免同一个 identifier 在不同 family 中套错 template。- default template 处理未知情况,包括默认 Complication identifier。
核心启发
1. 做一个“下一次行动”表盘入口
- 做什么:把下一次会议、下一班车、下一次服药或下一次潮汐放到表盘上,并让 entry 提前于真实开始时间出现。
- 为什么值得做:session 的观鲸时间线说明,Complication entry 不必等事件开始才显示,可以提前给用户准备时间。
- 怎么开始:把业务事件转成
CLKComplicationTimelineEntry,用getTimelineEntries(after:limit:withHandler:)提供未来 entry。
2. 为同一个 App 拆出多个数据维度
- 做什么:一个运动 App 同时提供“当前心率”“今日进度”“开始记录”三个 Complication。
- 为什么值得做:watchOS 7 允许单个 App 声明多个
CLKComplicationDescriptor,Whale Watch 示例也把观测站、记录入口、季节数据拆开。 - 怎么开始:在
getComplicationDescriptors(withHandler:)中为每个维度分配 identifier,并用userInfo携带后续渲染需要的数据。
3. 用 provider 把同一份内容适配多个 family
- 做什么:同一条倒计时在大表盘上显示完整时间,在小角标里自动缩短。
- 为什么值得做:
CLKDateTextProvider和CLKRelativeDateTextProvider会根据空间选择合适文本,减少手写布局分支。 - 怎么开始:先把文本、图片、gauge 都封装成 ClockKit provider,再把 provider 交给对应 template。
4. 给共享表盘和旧用户准备 fallback
- 做什么:当用户从旧版 Complication 或共享表盘进入时,仍显示 App 图标、热门数据或原有默认信息。
- 为什么值得做:session 明确要求支持
CLKDefaultComplicationIdentifier,否则用户可能在表盘上看到损坏的 Complication。 - 怎么开始:在 template 创建逻辑的
default分支返回一个可识别的默认 template,不要直接返回空内容。
5. 把 SwiftUI 用在最需要画布的位置
- 做什么:为矩形 Graphic family 做一个小图表,比如最近 7 天记录、趋势或完成度。
- 为什么值得做:session 示例在
SeasonData的.graphicRectangular分支使用CLKComplicationTemplateGraphicRectangularFullView承载 SwiftUI chart。 - 怎么开始:先限定一个只支持
.graphicRectangular的 descriptor,再在(family, identifier)分支里返回对应 SwiftUI template。
关联 Session
- Build complications in SwiftUI — 继续讲 Graphic Complication 里的 SwiftUI view、预览和视觉设计。
- Keep your complications up to date — 补上 timeline 刷新、后台刷新、URLSession 和推送更新策略。
- Meet Watch Face Sharing — 讲如何把包含 App Complication 的表盘从 App 或网页分享给用户。
- Create quick interactions with Shortcuts on watchOS — 讲 watchOS 上从 Complication 触发快捷操作和 App intents 的链路。
评论
GitHub Issues · utterances