WWDC Quick Look 💓 By SwiftGGTeam
Bring widgets to new places

Bring widgets to new places

观看原视频

Highlight

iOS 17 和 macOS Sonoma 引入了 StandBy、桌面 Widget 和 iPad 锁定屏幕等新场景,WidgetKit 新增了 containerBackgroundcontentMarginswidgetAccentablecontainerBackgroundRemovable 等 API,让 Widget 能自动适配不同环境的背景、边距和渲染风格。


核心内容

问题:Widget 曾经只有主屏幕一个归宿

iOS 14 引入 Widget 时,Widget 只有一个展示位置:主屏幕。开发者只需要考虑一种背景风格、一套边距规则、一种颜色渲染方式。

现在情况变了。iOS 17 带来了 StandBy 模式——iPhone 横置充电时,Widget 出现在深色全屏界面上。macOS Sonoma 允许 Widget 直接放在桌面上。iPad 的锁定屏幕也开始支持 Widget。

每个新场景对 Widget 的外观要求都不一样。StandBy 模式下系统会智能地为 Widget 着色,桌面 Widget 可能需要去掉背景色融入壁纸,而主屏幕 Widget 需要保持原有的鲜艳背景。一份代码适配所有环境,成了新难题。

Apple 的方案:环境感知的 Widget API

Apple 在 WidgetKit 中增加了三个核心能力:

  1. 容器背景containerBackground):让 Widget 知道自己的背景应该是什么风格,系统根据展示位置自动调整。
  2. 内容边距禁用contentMarginsDisabled):在需要无边距渲染时(如 StandBy),由开发者自行控制内边距。
  3. 强调色渲染widgetAccentable):在 StandBy 等深色智能着色模式下,标记哪些文本和元素应该被系统着色。

另外还有 containerBackgroundRemovable,让桌面 Widget 的背景可以被系统移除,实现”透明”效果融入壁纸。

这些 API 的共同思路是:开发者声明意图,系统根据运行环境决定具体渲染方式。


详细内容

禁用默认边距(02:08

iOS 17 中,Widget 默认会带有系统添加的内容边距。对于需要完全控制布局的 Widget——比如用纯色或渐变铺满整个 Widget 的——这个默认边距会留下难看的留白。

contentMarginsDisabled() 关闭默认边距,然后通过 widgetContentMargins 环境值手动控制边距:

struct SafeAreasWidgetView: View {
    @Environment(\.widgetContentMargins) var margins

    var body: some View {
        ZStack {
            Color.blue
            Group {
                Color.lightBlue
                Text("Hello, world!")
            }
                .padding(margins)
        }
    }
}

struct SafeAreasWidget: Widget {
    var body: some WidgetConfiguration {
        StaticConfiguration(...) {_ in
            SafeAreasWidgetView()
        }
        .contentMarginsDisabled()
    }
}

关键点

  • contentMarginsDisabled()WidgetConfiguration 上调用,关闭系统默认边距。
  • @Environment(\.widgetContentMargins) 读取系统原本会应用的边距值,让你自行决定哪里加 padding。
  • 这种模式适合全幅背景色(Color.blue)的 Widget——内层内容手动加边距,外层背景铺满。

容器背景:一套代码适配所有场景(03:19

containerBackground(for:alignment:content:) 是 iOS 17 / macOS Sonoma 中最关键的 Widget 新 API。它替代了之前直接在 View 上设置背景的做法。

struct EmojiRangerWidgetEntryView: View {
    var entry: Provider.Entry

    @Environment(\.widgetFamily) var family

    var body: some View {
        switch family {
        case .systemSmall:
            ZStack {
                AvatarView(entry.hero)
                    .widgetURL(entry.hero.url)
                    .foregroundColor(.white)
            }
            .containerBackground(for: .widget) {
                Color.gameBackground
            }
        }
        // additional cases
    }
}

关键点

  • containerBackground(for: .widget) 告诉系统”这段内容是这个 Widget 的背景”。系统根据 Widget 的展示环境决定如何渲染它。
  • 在 StandBy 模式下,这个背景可能被移除或淡化;在桌面上,用户可以选择去掉背景让 Widget 融入壁纸。
  • 不指定 for: .widget 的普通背景(如 ZStack 中的 Color.blue)不会受到系统智能调整。
  • @Environment(\.widgetFamily) 用来区分小、中、大和 accessory 系列尺寸,同一个 Widget 入口在不同尺寸下走不同布局分支。

强调色:让 Widget 在 StandBy 中自动着色(03:48

StandBy 模式下,系统会对 Widget 进行智能着色——将彩色内容转为适应深色环境的单色或双色调风格。但不是所有元素都应该被着色。widgetAccentable() 标记出那些需要被系统着色的文本:

var body: some View {
    switch family {
    case .accessoryRectangular:
        HStack(alignment: .center, spacing: 0) {
            VStack(alignment: .leading) {
                Text(entry.hero.name)
                    .font(.headline)
                    .widgetAccentable()
                Text("Level \(entry.hero.level)")
                Text(entry.hero.fullHealthDate, style: .timer)
            }.frame(maxWidth: .infinity, alignment: .leading)
            Avatar(hero: entry.hero, includeBackground: false)
        }
        .containerBackground(for: .widget) {
            Color.gameBackground
        }
    // additional cases
}

关键点

  • widgetAccentable() 标记文本元素,告诉系统在 StandBy 着色模式下优先着色这些内容。
  • 没有被标记的文本在着色模式下保持较淡的视觉效果,形成层次感。
  • accessoryRectangular 是 Lock Screen / StandBy 中使用的矩形 Widget 尺寸——紧凑、信息密度高。
  • 这个修饰符仅在系统执行智能着色时才生效,常规主屏幕 Widget 不受影响。

可移除背景:让 Widget 融入 macOS 桌面(04:22

macOS Sonoma 允许用户将 Widget 放在桌面上。用户可以选择”去掉 Widget 背景”,让它看起来像桌面的一部分。对于照片类 Widget,去掉背景可以让照片直接”浮”在壁纸之上。

struct PhotoWidget: Widget {
    public var body: some WidgetConfiguration {
        StaticConfiguration(...) { entry in
            PhotoWidgetView(entry: entry)
        }
        .containerBackgroundRemovable(false)
    }
}

关键点

  • containerBackgroundRemovable(false) 声明”我这个 Widget 的背景不能移除”。照片类 Widget 通常设为 false,因为照片本身就是内容,去掉背景等于去掉 Widget。
  • 设为 true(默认值)时,用户在 macOS 桌面上可以选择去掉 Widget 背景。
  • 这个 API 只在 macOS Sonoma 上有实际效果,但代码在所有平台上都可以写——系统自动处理平台差异。

核心启发

基于这组新 API,以下是几个可以动手的方向:

  • 做一个 StandBy 专用时钟 Widget:利用 widgetAccentable() 让时间数字在 StandBy 着色模式下保持突出,其他装饰元素淡化。用 containerBackground(for: .widget) 提供深色背景,让 Widget 在 StandBy 的深色环境中看起来是原生组件。实现入口:StaticConfiguration + accessoryRectangular 布局。

  • 升级现有 Widget 的桌面体验:如果你的 App 已有主屏幕 Widget,只需加两行代码就能让它在 macOS Sonoma 桌面上表现良好:把所有背景色移到 .containerBackground(for: .widget) 中,给关键文本加 .widgetAccentable()。不需要写任何平台判断代码。

  • 做一个照片 Widget 适配桌面:用 containerBackgroundRemovable(false) 阻止系统移除照片 Widget 的背景。用户把 Widget 拖到桌面后,照片无缝融入壁纸。结合 PhotoPicker 让用户选择展示的相册。

  • 做一个”无边距”天气 Widget:用 contentMarginsDisabled() + widgetContentMargins 手动控制边距,实现渐变背景铺满整个 Widget 区域的效果。适合做沉浸式天气展示——天空渐变从边缘到边缘。

  • 为现有 Widget 添加 accessory 系列支持:如果你的 Widget 只支持 systemSmall/Medium/Large,加上 accessoryRectangularaccessoryCircular 两个 case,就能同时出现在 iPhone 的 Lock Screen 和 StandBy 上。复用数据模型,只改布局。


关联 Session

评论

GitHub Issues · utterances