WWDC Quick Look 💓 By SwiftGGTeam
Go further with Complications in WidgetKit

Go further with Complications in WidgetKit

观看原视频

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:06accessoryCorner 是 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 可以是 TextGaugeProgressView。表盘会取出 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 显示当前整数值。
  • minimumValueLabelmaximumValueLabel 给表盘提供范围端点。
  • 当 label 出现时,圆形主体会自动缩小,给弧形内容留空间。

accessoryCircular:用 showsWidgetLabel 适配有表圈和无表圈的位置

04:24accessoryCircular 也能显示 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。
  • mgFormatterinMG() 保证 label 文案继续使用毫克单位。

05:47)Extra Large 表盘也使用 accessoryCircular。系统会自动把圆形内容放大。Apple 的建议很明确:不要因为画布变大就塞更多信息,内容层级应和普通圆形 Complication 一致。

从 ClockKit 迁移到 WidgetKit

06:45)迁移分两步。第一步是把旧 ClockKit 模板改写成 WidgetKit 的 SwiftUI view。watchOS 9 把旧的 12 个 ClockKit family 收敛成 4 个 WidgetKit family:accessoryRectangularaccessoryCircularaccessoryCorneraccessoryInline

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。
  • kindextensionBundleIdentifier 仍然定位新的 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

评论

GitHub Issues · utterances