Highlight
watchOS 9 把 WidgetKit 带到了手表的 Complications 上,和 iOS 16 的锁屏 Widget 共享同一套 API 基础。但手表有几个独有的需求:表盘角落的位置(accessoryCorner)需要同时显示圆形内容和弧形辅助标签;圆形 Complication 在某些表盘上有额外的表圈空间可以显示文字(比如 Infograph 表盘)。
核心内容
做 Apple Watch 的 Complication,难点不只在数据。表盘上的空间很小,同一个内容会出现在角落、表圈、圆形槽位、Extra Large 表盘里。旧的 ClockKit 还要求开发者面对很多模板和家族,迁移成本高。
watchOS 9 的变化,是把表盘 Complication 接到 WidgetKit。开发者继续写 SwiftUI view、timeline 和 configuration。系统负责把内容放到不同表盘位置,并让旧 ClockKit Complication 自动升级到新的 WidgetKit 版本。
这场 session 只讲 watchOS 独有的部分。重点有三个:accessoryCorner 的弧形辅助内容、accessoryCircular 在 Infograph 表盘上的额外标签、以及 CLKComplicationWidgetMigrator 的迁移映射。
Coffee Tracker 示例贯穿全场。它记录当天咖啡、茶和汽水摄入,并追踪体内咖啡因。这个例子适合 Complication:用户看一眼表盘,就知道当前咖啡因水平。
详细内容
accessoryCorner:圆形主体加弧形辅助内容
(03:06)accessoryCorner 是 watchOS 9 为表盘角落新增的 WidgetKit family。它的圆形主体按普通 SwiftUI 渲染,辅助内容交给表盘绘制。先看只有主体的版本。
struct CornerView: View {
let value: Double
var body: some View {
ZStack {
AccessoryWidgetBackground()
Image(systemName: "cup.and.saucer.fill")
.font(.title.bold())
.widgetAccentable()
}
}
}
关键点:
CornerView是一个 SwiftUI view,输入值是value,后面会用于 Gauge。ZStack把背景和图标叠在一起,形成角落 Complication 的圆形主体。AccessoryWidgetBackground()提供系统认可的 accessory 背景。Image(systemName:)使用 SF Symbol 展示咖啡杯图标。.widgetAccentable()让图标参与 WidgetKit 的强调色渲染。
(03:27)要加弧形内容,watchOS 9 提供 widgetLabel。在 accessoryCorner 里,label 可以是 Text、Gauge 或 ProgressView。表盘会取出 label 内容,绘制成角落里的曲线标签或进度。
struct CornerView: View {
let value: Double
var body: some View {
ZStack {
AccessoryWidgetBackground()
Image(systemName: "cup.and.saucer.fill")
.font(.title.bold())
.widgetAccentable()
}
.widgetLabel {
Gauge(value: value,
in: 0...500) {
Text("MG")
} currentValueLabel: {
Text("\(Int(value))")
} minimumValueLabel: {
Text("0")
} maximumValueLabel: {
Text("500")
}
}
}
}
关键点:
.widgetLabel不把内容画进圆形主体,而是把辅助内容交给表盘。Gauge(value:in:)把咖啡因数值映射到0...500的范围。Text("MG")是 Gauge 的单位标签。currentValueLabel显示当前整数值。minimumValueLabel和maximumValueLabel给表盘提供范围端点。- 当 label 出现时,圆形主体会自动缩小,给弧形内容留空间。
accessoryCircular:用 showsWidgetLabel 适配有表圈和无表圈的位置
(04:24)accessoryCircular 也能显示 Gauge。普通圆形槽位没有额外表圈文字,核心信息需要放在主体里。
struct CircularView: View {
let value: Double
var body: some View {
Gauge(value: value,
in: 0...500) {
Text("MG")
} currentValueLabel: {
Text("\(Int(value))")
}
.gaugeStyle(.circular)
}
}
关键点:
CircularView使用同一个value表示咖啡因数值。Gauge仍然使用0...500的范围。currentValueLabel把当前值放进 Gauge。.gaugeStyle(.circular)把 Gauge 渲染成圆形 Complication 适合的样式。
(04:34)Infograph 表盘的圆形 Complication 有额外表圈空间。这里可以把长一点的文字放进 widgetLabel。
struct CircularView: View {
let value: Double
var body: some View {
let mg = value.inMG()
Gauge(value: value,
in: 0...500) {
Text("MG")
} currentValueLabel: {
Text("\(Int(value))")
}
.gaugeStyle(.circular)
.widgetLabel {
Text("\(mg, formatter: mgFormatter) Caffeine")
}
}
var mgFormatter: Formatter {
let formatter = MeasurementFormatter()
formatter.unitOptions = [.providedUnit]
return formatter
}
}
extension Double {
func inMG() -> Measurement<UnitMass> {
Measurement<UnitMass>(value: self, unit: .milligrams)
}
}
关键点:
let mg = value.inMG()把数字包装成Measurement<UnitMass>。- 主体仍然用圆形 Gauge 显示当前值。
.widgetLabel添加"Caffeine"文字,给 Infograph 的表圈位置使用。MeasurementFormatter保留传入单位,避免系统改成其他质量单位。inMG()明确把Double转成毫克。
(05:12)问题在于,同一个 accessoryCircular 会出现在不同表盘位置。有的显示 widget label,有的没有。watchOS 9 提供 showsWidgetLabel 环境值,让 view 根据当前位置选择内容密度。
struct CircularView: View {
let value: Double
@Environment(\.showsWidgetLabel) var showsWidgetLabel
var body: some View {
let mg = value.inMG()
if showsWidgetLabel {
ZStack {
AccessoryWidgetBackground()
Image(systemName: "cup.and.saucer.fill")
.font(.title.bold())
.widgetAccentable()
}
.widgetLabel {
Text("\(mg, formatter: mgFormatter) Caffeine")
}
}
else {
Gauge(value: value,
in: 0...500) {
Text("MG")
} currentValueLabel: {
Text("\(Int(value))")
}
.gaugeStyle(.circular)
}
}
var mgFormatter: Formatter {
let formatter = MeasurementFormatter()
formatter.unitOptions = [.providedUnit]
return formatter
}
}
extension Double {
func inMG() -> Measurement<UnitMass> {
Measurement<UnitMass>(value: self, unit: .milligrams)
}
}
关键点:
@Environment(\.showsWidgetLabel)读取当前位置是否会显示 widget label。showsWidgetLabel == true时,主体只放咖啡杯图标,把数值文字放到 label。showsWidgetLabel == false时,主体改用圆形 Gauge,避免用户丢失咖啡因信息。- 两个分支都来自同一个 view,不需要拆成两个 widget。
mgFormatter和inMG()保证 label 文案继续使用毫克单位。
(05:47)Extra Large 表盘也使用 accessoryCircular。系统会自动把圆形内容放大。Apple 的建议很明确:不要因为画布变大就塞更多信息,内容层级应和普通圆形 Complication 一致。
从 ClockKit 迁移到 WidgetKit
(06:45)迁移分两步。第一步是把旧 ClockKit 模板改写成 WidgetKit 的 SwiftUI view。watchOS 9 把旧的 12 个 ClockKit family 收敛成 4 个 WidgetKit family:accessoryRectangular、accessoryCircular、accessoryCorner、accessoryInline。
(08:25)第二步是告诉系统如何升级用户已经放在表盘上的旧 Complication。为此,ClockKit 新增了 CLKComplicationWidgetMigrator。应用更新后,表盘会检查 app bundle 里的 widget,并调用旧的 CLKComplicationDataSource 生成迁移配置。
(09:47)最直接的做法,是让现有 data source 自己提供 migrator。
var widgetMigrator: CLKComplicationWidgetMigrator {
self
}
关键点:
widgetMigrator是 ClockKit data source 上新增的迁移入口。- 返回
self表示当前 data source 自己实现迁移协议。 - 系统会用这个对象把旧 Complication descriptor 映射到新的 WidgetKit configuration。
(09:56)如果新的 WidgetKit Complication 是 static configuration,返回 static migration configuration。
func widgetConfiguration(from complicationDescriptor: CLKComplicationDescriptor) async -> CLKComplicationWidgetMigrationConfiguration? {
CLKComplicationStaticWidgetMigrationConfiguration(kind: "CoffeeTracker", extensionBundleIdentifier: widgetBundle)
}
关键点:
widgetConfiguration(from:)接收旧的CLKComplicationDescriptor。- 返回值是可选的
CLKComplicationWidgetMigrationConfiguration。 CLKComplicationStaticWidgetMigrationConfiguration用于 static WidgetKit configuration。kind: "CoffeeTracker"指向新的 widget kind。extensionBundleIdentifier指向包含 widget 的 extension bundle。
(10:03)如果新的 Complication 使用 intent configuration,返回 intent migration configuration。Apple 特别提醒,intent 定义需要同时包含在 watch app 和 widget extension 里,这样两个位置都能创建 intent 对象。
func widgetConfiguration(from complicationDescriptor: CLKComplicationDescriptor) async -> CLKComplicationWidgetMigrationConfiguration? {
CLKComplicationIntentWidgetMigrationConfiguration(kind: "CoffeeTracker", extensionBundleIdentifier: widgetBundle, intent: intent, localizedDisplayName: "Coffee Tracker")
}
关键点:
CLKComplicationIntentWidgetMigrationConfiguration对应 intent-based WidgetKit complication。kind和extensionBundleIdentifier仍然定位新的 widget。intent保存旧 Complication 对应的配置状态。localizedDisplayName给迁移后的配置提供显示名称。- intent 定义缺失时,watch app 或 extension 可能无法创建迁移所需的 intent 对象。
核心启发
-
做一个咖啡因表盘状态:用
accessoryCircular显示当前咖啡因值,在 Infograph 表圈通过widgetLabel显示 “Caffeine” 和毫克数。入口是Gauge、.gaugeStyle(.circular)和.widgetLabel。 -
做一个饮水或站立进度角落 Complication:用
accessoryCorner的圆形主体放图标,把Gauge放进widgetLabel。用户能在角落看到图标和弧形进度。 -
做一个同 view 多表盘适配:在圆形 Complication 里读取
@Environment(\.showsWidgetLabel)。有表圈时主体放图标,没表圈时主体放 Gauge,保证信息不丢。 -
做一次 ClockKit 到 WidgetKit 的平滑迁移:先把旧模板改成 SwiftUI view,再实现
CLKComplicationWidgetMigrator。用户更新 app 后,原来放在表盘上的旧 Complication 会升级到新的 WidgetKit 版本。 -
做一个克制的 Extra Large 版本:复用普通
accessoryCircular内容,让系统放大。不要在 Extra Large 画布里新增额外层级,只优化字号、符号和对齐。
关联 Session
- Complications and widgets: Reloaded — 介绍 WidgetKit 在 iOS 16 锁屏和 watchOS 9 Complication 上的通用基础。
- What’s new in SwiftUI — 梳理 SwiftUI 2022 年更新,理解 WidgetKit view 写法背后的框架变化。
- Compose custom layouts with SwiftUI — 讲 SwiftUI 布局能力,适合继续优化紧凑表盘界面。
- Adopt Variable Color in SF Symbols — 讲 SF Symbols 的可变颜色,适合为 Complication 图标增加状态表达。
评论
GitHub Issues · utterances