Go further with Complications in WidgetKit
Swift & UI 进阶 20m

WidgetKit Complications 进阶:accessoryCorner、widgetLabel 与 ClockKit 迁移指南

Go further with Complications in WidgetKit

2022年6月6日

在 Apple 官方观看视频

一句话判断

如果你已经在用 WidgetKit 做表盘 Complications,这个 session 帮你搞定 watchOS 独有的 accessoryCorner 家族和 widgetLabel 辅助内容,并给出了完整的 ClockKit 迁移方案——12 个 ClockKit 家族精简到 4 个 WidgetKit 家族。

这场 Session 讲了什么

watchOS 9 把 WidgetKit 带到了手表的 Complications 上,和 iOS 16 的锁屏 Widget 共享同一套 API 基础。但手表有几个独有的需求:表盘角落的位置(accessoryCorner)需要同时显示圆形内容和弧形辅助标签;圆形 Complication 在某些表盘上有额外的表圈空间可以显示文字(比如 Infograph 表盘)。

Session 引入了 widgetLabel 这个 view modifier 来处理辅助内容。在 accessoryCorner 上,你可以用 widgetLabel 添加弧形文字、进度条或 Gauge,系统会自动把主内容缩小来腾出空间。在 accessoryCircular 上,widgetLabel 用于在表圈的额外空间里显示信息。新增的 showsWidgetLabel 环境变量让你根据当前表盘是否显示辅助内容来动态调整布局——这在 Infograph(有表圈)和其他表盘(没有表圈)之间切换时特别有用。

迁移部分覆盖了从 ClockKit 到 WidgetKit 的完整流程。ClockKit 的 12 个 Complication 家族被精简为 4 个:accessoryRectangular、accessoryCircular、accessoryCorner 和 accessoryInline。新增的 CLKComplicationWidgetMigrator 协议让你的 App 更新后,用户表盘上已有的旧 Complication 自动迁移到新的 WidgetKit 版本,无需手动操作。

值得深挖的点

accessoryCorner 是 watchOS 独有的第五个 Widget 家族。 和其他三个 iOS 共享的家族不同,accessoryCorner 有独特的视觉形态:圆形主内容 + 弧形辅助内容。它的渲染由表盘控制——你只负责提供 SwiftUI 内容和 widgetLabel,表盘决定最终的弧形样式。这种设计意味着同一个 Complication 在不同表盘上可能看起来完全不同。我觉得这个设计很聪明,开发者只需要关心内容,不用关心几十种表盘的渲染差异。

ClockKit 到 WidgetKit 的迁移不只是重写代码,还涉及用户体验的无缝过渡。 widgetMigrator 协议的关键在于,当你的 App 更新到 Watch 后,系统会自动检查你的 Widget Extension,然后启动 ClockKit 的数据源来生成迁移配置。这意味着即使你已经完全用 WidgetKit 重写了 Complication,你的 ClockKit 代码还不能删——至少在所有用户都迁移完毕之前。Session 特别提到,如果有人分享了包含你旧 ClockKit Complication 的自定义表盘,系统会再次请求迁移。所以你的迁移逻辑需要保持一致性,不能依赖临时状态。

代码片段

构建 accessoryCorner Complication:

// accessoryCorner:圆形内容 + 弧形辅助标签
struct CoffeeCornerEntry: View {
    var entry: CoffeeProvider.Entry
    
    var body: some View {
        ZStack {
            // 圆形主内容,自动裁剪为圆形
            Image(systemName: "cup.and.saucer.fill")
                .foregroundColor(.brown)
        }
        .widgetLabel {
            // 弧形辅助内容:可以是 Text、Gauge 或 ProgressView
            Gauge(value: entry.caffeineLevel) {
                Text("\(entry.caffeineLevel, specifier: "%.0f")mg")
            }
        }
    }
}

用 showsWidgetLabel 适配不同表盘:

struct CoffeeCircularEntry: View {
    var entry: CoffeeProvider.Entry
    @Environment(\.showsWidgetLabel) var showsWidgetLabel
    
    var body: some View {
        if showsWidgetLabel {
            // Infograph 表盘:有表圈空间,显示简洁图标
            // 详细信息放在 widgetLabel 里
            Image(systemName: "cup.and.saucer.fill")
                .widgetLabel {
                    Text("Caffeine: \(entry.caffeineLevel, specifier: "%.0f")mg")
                }
        } else {
            // 没有表圈的表盘:在圆形区域内显示所有信息
            Gauge(value: entry.caffeineLevel) {
                Text("\(entry.caffeineLevel, specifier: "%.0f")mg")
            }
        }
    }
}

ClockKit 迁移:自动迁移用户表盘上的旧 Complication:

// 在 CLKComplicationDataSource 中实现 WidgetMigrator
class ComplicationController: NSObject, CLKComplicationDataSource {
    
    // 新增的 widgetMigrator 属性
    var widgetMigrator: (any CLKComplicationWidgetMigrator)? {
        ComplicationMigrator()
    }
}

struct ComplicationMigrator: CLKComplicationWidgetMigrator {
    func migration(
        for complicationDescriptor: CLKComplicationDescriptor
    ) -> CLKComplicationStaticWidgetMigrationConfiguration? {
        // 把旧的 ClockKit descriptor 映射到新的 WidgetKit configuration
        return CLKComplicationStaticWidgetMigrationConfiguration(
            kind: "CoffeeTrackerWidget",
            extensionBundleIdentifier: "com.example.watch.CoffeeTracker.extension"
        )
    }
}

最佳实践

  • 如果你同时支持 ClockKit 和 WidgetKit,先确保 WidgetKit Complication 完善,然后添加 widgetMigrator 实现自动迁移。
  • 使用 showsWidgetLabel 环境变量来适配有表圈和无表圈的表盘,而不是创建两个不同的 Widget。
  • Extra Large 表盘使用 accessoryCircular 家族并自动放大内容,不要利用大画布堆砌信息——保持和普通圆形 Complication 相同的内容层级。
  • 为 intent-based 的 Complication 迁移时,确保 intent 定义同时包含在 Watch App 和 Widget Extension 的 bundle 中。
  • 测试时用不同表盘验证 accessoryCorner 的辅助内容渲染效果。

还有什么值得关注

  • accessoryInline 的内容由表盘提取渲染——你提供 Text 和 Image,表盘自己决定渲染样式(平面或弧形)。
  • ClockKit 的 12 个家族精简映射:Graphic Bezel/Circular/Corner Large → accessoryCircular,Modular Large/Utilitarian Small Flat → 不再需要独立家族。
  • widgetLabel 在 accessoryRectangular 上不生效,因为矩形 Complication 本身就有足够的显示空间。
WWDC 2022