WWDC Quick Look 💓 By SwiftGGTeam
Complications and widgets: Reloaded

Complications and widgets: Reloaded

观看原视频

Highlight

watchOS 团队的 Devon 和 iOS 团队的 Graham 介绍了 WidgetKit 在 iOS 16 和 watchOS 9 上的重大扩展。核心变化是:watchOS 的复杂功能(Complications)现在基于 WidgetKit 构建,而 iOS 16 新增了锁屏上的 accessory 小组件。


核心内容

Complication 的目标很直接:在表盘上给用户一个一眼可读的高价值信息,并把点击带回 App 的相关位置(01:07)。过去 watchOS 开发者主要围绕 ClockKit 工作。iOS 开发者则用 WidgetKit 做主屏幕 widget。两套入口相近,代码和心智模型却分开。

iOS 16 和 watchOS 9 把这件事合并到 WidgetKit。Apple 给 WidgetFamily 增加了 accessoryRectangularaccessoryCircularaccessoryInline,watchOS 还额外有 accessoryCorner。前 3 个 family 同时出现在 iOS 锁屏和 watchOS 表盘上,开发者能共享 timeline、SwiftUI 视图和现有 Home Screen widget 的基础设施(01:58)。

这场 session 以 Emoji Rangers 为例,从一个已有 iOS Home Screen widget 出发,把同一个 Widget Extension 复制到 watchOS,再逐步适配小尺寸 family。重点不在“把大 widget 缩小”,而在重新决定每个尺寸只显示什么信息:圆形只显示头像和恢复进度,矩形显示名字、等级和倒计时,inline 则用 ViewThatFits 在不同宽度里选择合适文本(09:56)。

最后还要处理锁屏和常亮表盘的隐私环境。iOS 锁屏可能在设备锁定时显示或隐藏内容;watchOS 常亮状态会降低亮度和刷新频率。WidgetKit 提供 isLuminanceReducedprivacySensitive(),让同一个视图针对低亮度和隐私遮盖做局部调整(15:02)。


详细内容

1. 用 accessory family 共享 iOS 锁屏和 watchOS 表盘视图

02:11)新的 accessory family 都挂在 WidgetFamily 上。accessoryRectangular 适合多行文字、小图表;accessoryCircular 适合简短信息、仪表盘和进度;accessoryInline 是一行文字,出现在 watchOS 多个表盘位置以及 iOS 时间上方。

Emoji Rangers 在 Xcode Preview 中同时预览三个 family:

EmojiRangerWidgetEntryView(entry: SimpleEntry(date: Date(), relevance: nil, character: .spouty))
    .previewContext(WidgetPreviewContext(family: .accessoryCircular))
    .previewDisplayName("Circular")
EmojiRangerWidgetEntryView(entry: SimpleEntry(date: Date(), relevance: nil, character: .spouty))
    .previewContext(WidgetPreviewContext(family: .accessoryRectangular))
    .previewDisplayName("Rectangular")
EmojiRangerWidgetEntryView(entry: SimpleEntry(date: Date(), relevance: nil, character: .spouty))
    .previewContext(WidgetPreviewContext(family: .accessoryInline))
    .previewDisplayName("Inline")

#if os(iOS)

关键点:

  • 第 1 行创建同一个 EmojiRangerWidgetEntryView,输入同一份 timeline entry。
  • .accessoryCircular 用圆形尺寸检查头像、仪表盘和进度内容是否塞得下。
  • .accessoryRectangular 用矩形尺寸检查多行文字和图片组合。
  • .accessoryInline 用一行文字尺寸检查是否会被截断。
  • #if os(iOS) 来自 demo 里的平台分支,原因是 watchOS 不支持所有 iOS system family(08:38)。

2. 用 widgetAccentable 和背景适配渲染模式

03:25)Accessory widget 会被系统以 full color、accented、vibrant 三种模式渲染。watchOS 可以完整显示颜色,也可以把内容分成强调组和平常组;iOS 锁屏的 vibrant 模式会先去饱和,再根据锁屏背景映射成材质效果。

widgetAccentable() 用来告诉系统哪些内容属于强调组:

VStack(alignment: .leading) {
    Text("Headline")
        .font(.headline)
        .widgetAccentable()
    Text("Body 1")
    Text("Body 2")
}.frame(maxWidth: .infinity, alignment: .leading)

关键点:

  • VStack(alignment: .leading) 把多行文字按左侧对齐,符合矩形 accessory 的阅读方式。
  • Text("Headline") 是最重要的信息,例如角色名字或状态标题。
  • .font(.headline) 使用系统 font style,系统会在 iOS 和 watchOS 上给出各自合适的字重和字形(12:22)。
  • .widgetAccentable() 标记标题进入强调颜色组,accented 模式下系统会单独着色。
  • frame(maxWidth: .infinity, alignment: .leading) 让内容占满可用宽度,避免矩形区域里出现不必要的居中。

有些 accessory 需要一个系统调校过的背景。Session 用日历圆形样式演示了 AccessoryWidgetBackground

ZStack {
    AccessoryWidgetBackground()
    VStack {
        Text("MON")
        Text("6")
            .font(.title)
    }
}

关键点:

  • ZStack 把背景和文字叠在同一个圆形区域里。
  • AccessoryWidgetBackground() 由系统根据 full color、accented、vibrant 模式切换外观。
  • 第一个 Text 放星期,第二个 Text 放日期数字。
  • .font(.title) 让日期数字成为视觉主信息。

3. watchOS 需要 IntentRecommendation

09:12)iOS 的 widget 编辑界面能完整配置 Intent。watchOS 需要开发者提供预配置列表。Emoji Rangers 通过重写 IntentTimelineProviderrecommendations 方法返回这些推荐项。

return recommendedIntents()
    .map { intent in
        return IntentRecommendation(intent: intent, description: intent.hero!.displayString)
    }

关键点:

  • recommendedIntents() 是项目已有的推荐 Intent 来源。
  • .map { intent in ... } 把每个 Intent 转成 watchOS 可展示的推荐项。
  • IntentRecommendation(intent:description:) 把 Intent 本身和编辑界面里显示的描述绑在一起。
  • intent.hero!.displayString 来自 Emoji Rangers 的角色配置,用于告诉用户这个推荐项对应哪个 hero。

4. 小尺寸界面依赖自动更新的 ProgressView 和日期文本

10:37)圆形 accessory 适合显示一个头像,再用边缘进度表达恢复时间。普通做法需要频繁生成 timeline entry,才能让进度动起来。SwiftUI 的自动更新 ProgressView 接受一个日期区间,系统会保持进度更新,因此这里只需要一个 timeline entry。

ProgressView(interval: entry.character.injuryDate...entry.character.fullHealthDate,
             countdown: false,
             label: { Text(entry.character.name) },
             currentValueLabel: {
    Avatar(character: entry.character, includeBackground: false)
})
.progressViewStyle(.circular)

关键点:

  • interval: 传入受伤时间到完全恢复时间的闭区间。
  • countdown: false 表示进度按区间推进,而非倒计时样式。
  • label 提供角色名字,给进度视图一个语义标签。
  • currentValueLabel 把当前值区域替换成角色头像。
  • .progressViewStyle(.circular) 让进度贴合圆形 accessory 的外观。

矩形 accessory 空间更大,适合放三行信息和头像:

case .accessoryRectangular:
    HStack(alignment: .center, spacing: 0) {
        VStack(alignment: .leading) {
            Text(entry.character.name)
            Text("Level \(entry.character.level)")
            Text(entry.character.fullHealthDate, style: .timer)
        }.frame(maxWidth: .infinity, alignment: .leading)
        Avatar(character: entry.character, includeBackground: false)
    }

关键点:

  • case .accessoryRectangular 为矩形 family 单独提供布局。
  • HStack 左边放文字,右边放头像。
  • 第一行显示角色名,第二行显示等级。
  • Text(entry.character.fullHealthDate, style: .timer) 使用自动更新的日期文本显示剩余时间。
  • Avatar(... includeBackground: false) 避免在小尺寸区域里叠加多余背景。

5. 用 ViewThatFits 处理 inline 宽度差异

13:13)Inline accessory 是一行文本,watchOS 表盘里的可用宽度差异很大。ViewThatFits 接受一组候选 view,系统选择第一个能完整放下、不会截断或裁剪的版本。

ViewThatFits {
    Text("\(entry.character.name) is resting, combat-ready in \(entry.character.fullHealthDate, style: .relative)")
    Text("\(entry.character.name) ready in \(entry.character.fullHealthDate, style: .timer)")
    Text("\(entry.character.avatar) \(entry.character.fullHealthDate, style: .timer)")
}

关键点:

  • 第一个 Text 信息最完整,适合 iOS 锁屏或较宽的 watch slot。
  • 第二个 Text 缩短文案,把核心保留为角色名和倒计时。
  • 第三个 Text 用头像替代名字,服务最窄的表盘位置。
  • 候选顺序从长到短,确保有空间时优先展示更完整的信息。

6. 低亮度和隐私遮盖要拆开处理

16:18)watchOS 常亮状态会进入低亮度和低刷新频率。开发者能用 isLuminanceReduced 判断当前环境,移除时间敏感内容,换成更适合常亮显示的内容。

@Environment(\.isLuminanceReduced)
var isLuminanceReduced

var body: some View {
    if isLuminanceReduced {
        Text("🙈").font(.title)
    } else {
        Text("🐵").font(.title)
    }
}

关键点:

  • @Environment(\.isLuminanceReduced) 从 SwiftUI 环境读取低亮度状态。
  • if isLuminanceReduced 进入 always-on 的低亮度分支。
  • 低亮度分支显示更静态的内容,避免依赖高频更新。
  • 普通分支显示完整内容。

隐私遮盖则针对敏感信息。默认 privacy mode 会显示 placeholder 的 redacted 版本;如果只有部分内容敏感,可以给具体 view 加 .privacySensitive()16:52)。

VStack(spacing: -2) {
    Image(systemName: "heart")
        .font(.caption.bold())
        .widgetAccentable()
    Text("\(currentHeartRate)")
        .font(.title)
        .privacySensitive()
}

关键点:

  • Image(systemName: "heart") 是心率图标,本身不包含用户隐私值。
  • .widgetAccentable() 让图标适配 accessory 的强调颜色。
  • Text("\(currentHeartRate)") 显示具体心率,这一项是敏感数据。
  • .privacySensitive() 只标记心率文本,系统遮盖时仍保留图标。

核心启发

  1. 做什么:把已有 Home Screen widget 扩展到锁屏和 Apple Watch。 为什么值得做: 新的 accessory family 和 watchOS Widget Extension 能复用现有 WidgetKit timeline 与 SwiftUI 视图。 怎么开始: 复制现有 iOS widget extension 为 watchOS target,按平台分支声明 supportedFamilies,先补齐 .accessoryCircular.accessoryRectangular.accessoryInline 的预览。

  2. 做什么:为状态型 App 做一个圆形恢复进度 complication。 为什么值得做: ProgressView(interval:) 能由系统自动更新,适合健身恢复、专注计时、游戏角色冷却这类时间区间。 怎么开始: 在圆形 family 中把核心图标放进 currentValueLabel,把开始和结束时间传给 ProgressView(interval:countdown:label:currentValueLabel:)

  3. 做什么:给一行状态文案写三档长度。 为什么值得做: Inline accessory 的宽度会随表盘和锁屏位置变化,单一长句很容易被截断。 怎么开始:ViewThatFits 按“完整句子、短句、图标加时间”的顺序提供候选 view,并在 Xcode Preview 中检查 .accessoryInline

  4. 做什么:为锁屏数据建立隐私分级。 为什么值得做: iOS 锁屏和 watchOS 常亮状态都可能展示在旁人可见的环境里,WidgetKit 支持只遮盖敏感 view。 怎么开始: 把心率、余额、消息内容等具体数值标记为 .privacySensitive(),图标、标题、类别名称保持可见。

  5. 做什么:为 always-on watch face 准备低亮度版本。 为什么值得做: 常亮状态刷新频率更低,时间敏感文字和动态进度需要降级。 怎么开始: 读取 @Environment(\.isLuminanceReduced),在低亮度分支中移除倒计时、动画或快速变化数据,保留静态图标和概括状态。


关联 Session

评论

GitHub Issues · utterances