What's new in widgets
Widget & Live Activities 进阶 35m

小组件新特性

What's new in widgets

2025年6月12日

在 Apple 官方观看视频

一句话判断

小组件终于能打字了——这个变化比听起来重要得多,它意味着主屏幕上的那块小方格第一次有了”做事”的能力,而不只是”展示”。

这场 Session 讲了什么

WWDC 2023 给小组件加了 Button 和 Toggle,算是开了个口子。但说实话,“只能点一下”的交互撑不起什么正经场景。这次 Session 把 TextField、Slider、Stepper 塞进了小组件,性质完全不同了。用户可以在主屏幕直接输入文字、拖动滑块、调整数值——不用打开 App。待办事项直接在小组件里记一条,音量在小组件里调一下,这些过去需要四步跳转的操作,现在一步到位。

另一个大变化是 DynamicOptionsProvider。以前小组件的配置参数只能是写死的枚举,天气小组件支持哪些城市得在代码里列出来。现在可以从服务端动态拉了,用户配置小组件时系统会调你的 provider 拿最新数据。这不只是技术上的便利,它让小组件的配置逻辑真正跟用户数据挂钩了。

Live Activity 那边也有升级,但更偏视觉层面——渐变背景、进度环、自定义动画,锁屏和灵动岛的表现力上了一个台阶。同时加了 .accented 渲染模式,低电量或离线时小组件不会白屏,而是用单色强调色优雅降级。

值得深挖的点

TextField 进小组件:交互模型的质变

小组件的交互模型一直是”一次点击 = 一个 App Intent”,系统在幕后控制进程生命周期和内存预算。TextField 的加入打破了这个简单映射——用户输入是连续的、有上下文的,但小组件的执行环境是无状态的、短命的。

这意味着你不能把小组件当成一个迷你 App 来用。TextField 的 onSubmit 才会触发 App Intent,中间的输入过程完全在 SwiftUI 的渲染层完成,不涉及你的业务逻辑。这跟主应用里 @State 配合 ViewModel 的模式完全不同。设计上的隐含约束是:输入必须是”一句话能搞定”的事。写一条待办、输一个搜索词没问题,但填表单就别想了。

跟 2023 年的 Button/Toggle 相比,TextField 的系统开销也更大。文本渲染、键盘弹出、光标管理这些事都要吃内存和 CPU 预算。小组件的内存上限大约 30MB,频繁触发文本输入可能会被系统”教育”。所以实际落地时,建议把 TextField 用在刀刃上——高频刚需场景,而不是”有就加上”。

DynamicOptionsProvider:配置从编译时走向运行时

过去的 ConfigurationIntent 里,参数选项是编译时确定的。你定义一个 enum,列几个 case,完事。这对天气小组件选个温度单位(摄氏/华氏)够用,但对”选择你关注的球队”这种跟用户账户绑定的场景就是硬伤——你不可能把所有球队写死,也不可能在代码里维护一份实时的球员交易数据。

DynamicOptionsProvider 把选项的来源从代码搬到了网络。系统在用户长按小组件进入配置界面时调用你的 results() 方法,你从服务端拉数据返回就行。但这里有个容易忽略的坑:results() 的调用时机完全由系统控制,你没法预判。如果网络慢或挂了,用户看到的就是一个转圈圈或者空列表。所以必须做本地缓存作为降级——先返回上次缓存的数据,后台静默更新。

从架构角度看,这实际上是在小组件的配置层引入了一个异步数据源。你的小组件配置不再是纯静态的 Intent 定义,而是需要配合网络层、缓存层一起工作。对小项目来说可能是个负担,但对需要个性化配置的应用来说,这是刚需。

代码片段

1. 内联文本输入——待办事项小组件

场景:用户在主屏幕小组件里直接输入新任务,按回车提交。

struct TodoWidgetView: View {
    @State private var newTask: String = ""

    var body: some View {
        VStack {
            Text("待办事项").font(.headline)
            TextField("添加新任务...", text: $newTask)
                .onSubmit {
                    TodoStore.shared.addTask(newTask)
                    newTask = ""
                }
        }
    }
}

坑:onSubmit 触发的是 App Intent,小组件进程和主 App 进程是隔离的,别指望直接访问共享的内存状态——WidgetKit 通过 UserDefaults App Group 或进程间通信来同步数据。

2. 动态配置——服务端拉取城市列表

场景:天气小组件配置时,从服务端获取用户关注的城市列表供选择。

struct CityOptionProvider: DynamicOptionsProvider {
    func results() async throws -> [CityOption] {
        let cities = try await WeatherAPI.fetchAvailableCities()
        return cities.map { CityOption(id: $0.id, name: $0.name) }
    }
}

坑:results() 没有超时参数,如果服务端响应慢,系统可能会直接取消调用并显示空列表。务必实现本地缓存降级,永远别让网络失败变成配置界面的白屏。

3. .accented 降级渲染

场景:确保小组件在低电量模式下不会变成一块灰色废铁。

struct MyWidgetView: View {
    var body: some View {
        VStack {
            Image(systemName: "heart.fill")
                .foregroundStyle(.red) // 正常模式下显示红色
            Text("128")
                .font(.largeTitle)
        }
        // .accented 模式下系统会自动转为单色,确保数字和图标仍可辨认
    }
}

坑:.accented 模式下所有颜色都会被忽略,只保留形状和文字。如果你的小组件靠颜色区分状态(红=告警,绿=正常),降级后信息会丢失——用文字标签或不同图标来做冗余表达。

最佳实践

先把 TextField 用在一个最高频的输入场景上——比如 App 里用户每天都会做的事。不要贪多,一个就够了。先跑通从小组件输入到 App Intent 到数据持久化的完整链路,确认 App Group 的数据同步没问题,再扩展其他控件。

DynamicOptionsProvider 建议暂时只迁移那些确实需要动态数据的配置项。如果城市列表一年变一次,硬编码反而更稳定。把精力放在跟用户账户强绑定的参数上——关注列表、收藏夹、个性化偏好。缓存层必须有,而且第一次发版时就要带,别等用户反馈”配置界面空白”再补。

Live Activity 的视觉升级可以放后一步做。先把核心交互逻辑稳住,富媒体部分是锦上添花。但 .accented 降级建议现在就测——模拟低电量模式,确认小组件在单色下还能用。很多开发者会忽略这一步,直到用户投诉才发现小组件降级后变成了不可读的色块。

还有什么值得关注

  • Widget Bundle 改进:用户添加小组件时可以左右滑动切换变体,不再需要分别添加,对提供多种尺寸/样式的应用来说体验更连贯。
  • 富媒体 Live Activity 的自定义动画(ActivityAnimationModifier)让状态切换不再是生硬的文字替换,但动画的系统开销不小,别在灵动岛那个小地方搞太花哨的东西。
  • 小组件的交互预算仍然是硬约束——每次交互一个 App Intent,不能做连续多步操作,这个限制短期内不太可能放开。
WidgetKit 小组件 Live Activities SwiftUI 交互式