WWDC Quick Look 💓 By SwiftGGTeam
WidgetKit foundations

WidgetKit foundations

观看原视频

Highlight

WidgetKit 通过 Timeline(时间线)机制让 Widget 在独立进程中按需渲染,开发者只需提供数据条目和 SwiftUI 视图,系统负责在正确的时间展示正确的内容,同时通过 reload policy 控制刷新频率以平衡实时性与电池续航。

核心内容

为什么 Widget 需要独立进程

你的 App 可能用 SwiftUI 写,也可能用 UIKit 写,但 Widget 统一用 SwiftUI 构建。Widget 运行在独立的 Widget Extension 进程中,不是主 App 的一部分。

这意味着主 App 和 Widget 之间不能直接共享内存。数据传递必须通过 App Group 里的共享容器,比如共享的 UserDefaults 或数据库。(02:30

这个设计的代价是多了一层数据同步的复杂度,收益是系统可以随时渲染 Widget 而不必唤醒你的主 App。Widget 的视图被系统归档缓存,到点了直接展示,不需要你的代码持续运行。

好 Widget 的三个标准

Apple 把 memorable widget 的标准归结为三个词:(01:03

  • Glanceable(一目了然):用户扫一眼就能获取信息。天气 Widget 只显示当前温度和天气状况,不需要展开操作。
  • Relevant(相关):内容随时间、地点、用户习惯变化。日历 Widget 在早上显示当天会议,下午自动切换到晚间安排。
  • Personalizable(可个性化):用户可以配置显示什么内容。照片 Widget 让用户选择特定相册,只展示家庭露营的照片。

这三个标准决定了你的 Widget 会不会被用户长期留在主屏幕上。

Timeline:Widget 的心脏

WidgetKit 不向你的 Extension 索要”当前视图”,而是索要一组未来时间点的视图数据,这就是 Timeline。(03:12

每个 Timeline 由多个 Timeline Entry 组成,每个 Entry 携带特定时间点的数据。系统提前渲染这些视图并归档,到时间了直接展示。

Timeline Provider 需要处理三种状态:(05:26

  • Snapshot:Widget 画廊里的预览图。这是你的”第一印象”,要用真实且有吸引力的数据。比如读书 App 可以默认展示一本热门书籍的封面和进度。
  • Placeholder:Widget 首次加载时的占位视图。必须同步返回,不能读磁盘或走网络。SwiftUI 的 .redacted() 修饰符可以快速生成简化版视图。
  • Timeline:实际的时间线条目序列。每个条目包含特定时刻渲染所需的全部数据。

刷新策略:三种 reload policy

Timeline 的刷新行为由 reload policy 控制,三种选择对应三种场景:(07:55

  • atEnd:Timeline 条目耗尽后自动刷新。适合内容序列不固定、需要持续补充的场景,比如一天中不同时段的阅读提醒。
  • afterDate:指定具体时间点刷新。适合有明确刷新时刻的场景,比如阅读日程 Widget 在每天午夜重新计算后续安排。
  • never:不自动刷新。适合只在用户交互或 App 状态变化时才需要更新的场景,比如阅读进度 Widget。需要时通过 WidgetCenter 的 reload API 或推送通知显式触发。

WidgetKit 给每个 Widget 分配了刷新预算,预算大小受用户查看频率影响。前台频繁触发刷新可能被节流,App 进入后台前的最后一次 reload 调用通常是最佳时机。(10:27

如果内容有明确的起止时间、需要高频更新和推送提醒,比如体育赛事,应该考虑用 Live Activity 而不是 Widget。

Widget 与 App 的三种连接方式

Widget 不是孤岛,三种机制把它和主 App 绑在一起:(13:25

  • Deep Link:点击 Widget 直接进入 App 的特定页面。读书 Widget 点击后跳转到当前书籍的详情页,而不是 App 首页。
  • 可配置 Widget:用户可以选择 Widget 显示什么内容。天气 Widget 让用户选地点,读书 Widget 让用户选跟踪哪本书。
  • 交互元素:按钮和开关让用户直接在 Widget 上操作。完成阅读章节后点击 Widget 上的按钮直接打卡,不需要打开 App。

交互元素通过 App Intent 实现。Widget 视图被系统归档渲染,你的代码不在运行时执行,所以按钮和开关绑定的是 App Intent,由系统在用户交互时代为执行。(16:45

适配 Tinted 和 Clear 模式

iOS 支持 Tinted(着色)和 Clear(透明)两种系统定制模式。在这两种模式下,系统会用玻璃材质替换 Widget 背景,并对内容做色调处理。(17:23

大部分情况下 SwiftUI 自动处理得很好,但图片可能出问题。比如书籍封面在 Clear 模式下变成白色方块,因为系统无法正确着色这张图片。

解决方案是 .widgetAccentedRenderingMode(.fullColor) 修饰符,告诉系统这块区域保持原色渲染。(18:17

详细内容

创建第一个 Widget

03:50

Xcode 创建 Widget Extension 时会自动生成基础代码。一个最简单的 Widget 需要三个部分:Widget 结构体、Timeline Provider、SwiftUI 视图。

struct DailyReadingGoalWidget: Widget {
    let kind = "DailyReadingGoalWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(
            kind: kind,
            provider: DailyReadingGoalProvider()
        ) { entry in
            DailyReadingGoalView(book: entry.book,
                                 message: entry.message,
                                 timeOfDay: entry.timeOfDay)
            .environment(\.colorScheme, .dark)
            .containerBackground(for: .widget) {
                Background()
            }
        }
    }
}

关键点:

  • kind 是 Widget 的唯一标识符,系统用它来区分不同类型的 Widget。
  • StaticConfiguration 用于不可配置的 Widget。如果需要用户配置参数,改用 AppIntentConfiguration
  • provider 负责生成 Timeline Entry,是数据层的核心。
  • 闭包参数 entry 是单个时间点的数据,返回的 SwiftUI 视图会被系统归档。
  • .containerBackground(for: .widget) 声明了 Widget 的背景视图。这是 Tinted/Clear 模式适配的关键:系统需要知道哪个 View 是背景,才能用玻璃材质替换它。
  • .environment(\.colorScheme, .dark) 强制暗色模式,确保 Widget 在亮色主屏幕上的可读性。

限制 Widget 支持的尺寸

12:25

Widget 支持多种尺寸(family),包括 systemSmall、systemMedium、systemLarge,以及 iOS 27 新增的 systemExtraLargePortrait。建议尽可能支持更多尺寸,但不是所有 Widget 都适合所有尺寸。

struct DailyReadingGoalWidget: Widget {
    let kind = "DailyReadingGoalWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(
            kind: kind,
            provider: DailyReadingGoalProvider()
        ) { entry in
            DailyReadingGoalView(book: entry.book,
                                 message: entry.message,
                                 timeOfDay: entry.timeOfDay)
            .environment(\.colorScheme, .dark)
            .containerBackground(for: .widget) {
                Background()
            }
        }
        .supportedFamilies([.systemMedium])
    }
}

关键点:

  • .supportedFamilies 限制 Widget 可用的尺寸。这里只支持 systemMedium。
  • 同一个 Widget 可以复用同一个 Timeline Provider,只需要为不同尺寸提供不同的 SwiftUI 视图。
  • systemExtraLargePortrait 在 visionOS 26 首次引入,iOS 27、macOS 27、iPadOS 27 也支持了。这个竖向超大尺寸适合展示信息流或日程表。

14:03

默认点击 Widget 会打开 App 的首页。如果 Widget 展示的是特定内容,应该提供 Deep Link 直达该内容。

struct DailyReadingGoalWidget: Widget {
    let kind = "DailyReadingGoalWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(
            kind: kind,
            provider: DailyReadingGoalProvider()
        ) { entry in
            DailyReadingGoalView(book: entry.book,
                                 message: entry.message,
                                 timeOfDay: entry.timeOfDay)
            .environment(\.colorScheme, .dark)
            .containerBackground(for: .widget) {
                Background()
            }
            .widgetURL(URL(string: "bookclub://reading/\(book.bookID)"))
        }
        .supportedFamilies([.systemMedium])
    }
}

关键点:

  • .widgetURL 在 Widget 视图上附加一个 URL,点击 Widget 时系统把这个 URL 传给 App。
  • URL 里编码了书籍 ID,App 启动后解析这个 URL 直接跳转到对应书籍的详情页。
  • Deep Link 保持了 Widget 和 App 之间的上下文连续性,用户从 Widget 进入 App 时不会”迷路”。

图片在 Tinted 模式下的渲染控制

18:17

Tinted 模式下系统尝试把 Widget 内容转换成单色调,以保持主屏幕视觉统一。但这会破坏照片、封面、Logo 等需要保留原色的内容。

struct BookCoverImage: View {
    let imageName: String

    var body: some View {
        Image(imageName: bundle: .main)
            .widgetAccentedRenderingMode(.fullColor)
    }
}

关键点:

  • .widgetAccentedRenderingMode(.fullColor) 告诉系统这张图片保持全彩渲染,不参与色调处理。
  • 另一个选项是 .accented,让图片参与系统的色调处理。纯图标类内容适合用这个选项。
  • 滥用 .fullColor 会破坏 Tinted 模式的视觉统一性。只给真正需要保留原色的核心视觉元素使用,比如用户头像、书籍封面、品牌 Logo。
  • 背景和其他辅助元素应该跟随系统 Tint 变化,不要全局加 .fullColor

核心启发

  • 做一款阅读追踪 Widget:用 StaticConfiguration + afterDate 刷新策略,每天午夜重新计算阅读计划。支持 systemMedium 展示今日目标,systemExtraLargePortrait 展示未来三天的阅读安排。Deep Link 直达当前章节。入口 API:StaticConfigurationTimelineProviderwidgetURL

  • 做一款可配置的多城市天气 Widget:用 AppIntentConfiguration 让用户选择关注的城市,支持添加多个相同 Widget 的不同配置实例。一个主屏幕放三个 Widget 分别跟踪家里、公司、旅行目的地的天气。入口 API:AppIntentConfigurationAppIntent

  • 做一款交互式习惯打卡 Widget:在 Widget 上放按钮,点击直接完成今日习惯。按钮绑定 App Intent 更新数据,通过 WidgetCenter.reloadTimelines 刷新 Widget 状态。入口 API:ButtonAppIntentWidgetCenter

  • 适配现有 Widget 到 Tinted/Clear 模式:检查所有 Widget 图片,给需要保留原色的内容加 .widgetAccentedRenderingMode(.fullColor),给所有 Widget 根视图加 .containerBackground(for: .widget)。用 Xcode SwiftUI Preview 切换不同 rendering mode 快速验证效果。入口 API:widgetAccentedRenderingModecontainerBackground

  • 跨平台复用 Widget:iOS Widget 自动在 CarPlay 和 macOS(作为 remote widget)上可用。测试时确保 Deep Link 和交互在 macOS 上也能正常工作,交互手感在鼠标点击和触控之间保持一致。入口 API:supportedFamilieswidgetURL

关联 Session

评论

GitHub Issues · utterances