实现、审查或改进 WidgetKit 小组件和控件。适用于构建主屏幕、锁屏、待机或 CarPlay 小组件(使用时间线提供者);可配置小组件(使用 AppIntentTimelineProvider);交互式小组件或控制中心控件(使用 Button/Toggle 连接);WidgetKit 推送刷新、刷新预算、深度链接、智能叠放相关性、Liquid Glass/强调色渲染、小组件扩展设置、WidgetBundle、App Groups 和授权。
WidgetKit
为 iOS 26+ 构建主屏幕小组件、锁屏小组件、控制中心控件以及待机或 CarPlay 小组件界面。
将相邻框架的指导范围限定在 WidgetKit 集成中。仅当 ActivityKit 和 App Intents 直接连接到 WidgetKit 界面时才包含它们;将完整的生命周期、APNs content-state、Siri/Shortcuts/Spotlight 或实体建模工作交给同级的 activitykit 或 app-intents 技能。
有关时间线策略、基于推送的更新、Xcode 设置和高级模式,请参阅 references/widgetkit-advanced.md。
目录
- 工作流程
- Widget 协议和 WidgetBundle
- 配置类型
- TimelineProvider
- AppIntentTimelineProvider
- Widget 家族
- 交互式小组件 (iOS 17+)
- ActivityConfiguration 交接
- 控制中心小组件 (iOS 18+)
- 锁屏小组件
- 待机模式
- Widget URL 处理和深度链接
- 智能叠放相关性
- 设计模式
- iOS 26 新增功能
- 常见错误
- 审查清单
- 参考资料
工作流程
1. 创建新小组件
- 在 Xcode 中添加 Widget Extension 目标(File > New > Target > Widget Extension)。
- 启用 App Groups 以在应用和小组件扩展之间共享数据。
- 定义
TimelineEntry结构体,包含date属性和显示数据。 - 实现
TimelineProvider(静态)或AppIntentTimelineProvider(可配置)。 - 使用 SwiftUI 构建小组件视图,根据
WidgetFamily调整布局。 - 声明符合
Widget协议的结构体,包含配置和支持的家族。 - 在带有
@main注解的WidgetBundle中注册所有小组件。
2. 集成相邻界面
- 当应用有 Live Activity 时,在 widget bundle 中注册
ActivityConfiguration,但将ActivityAttributes、请求/更新/结束、APNscontent-state和 Dynamic Island 布局深度保留在activitykit中。 - 在 WidgetKit 视图或控件中放置
Button、Toggle、ControlWidgetButton和ControlWidgetToggle,但将意图建模、实体、查询、Siri、Shortcuts 和 Spotlight 保留在app-intents中。
3. 添加控制中心控件
- 为按钮复用
AppIntent/OpenIntent,或为开关复用SetValueIntent。 - 在 widget bundle 中创建
ControlWidgetButton或ControlWidgetToggle。 - 使用
StaticControlConfiguration或AppIntentControlConfiguration。
4. 审查现有小组件代码
按照本文档末尾的审查清单进行检查。
Widget 协议和 WidgetBundle
Widget
每个小组件都符合 Widget 协议,并从其 body 返回一个 WidgetConfiguration。
struct OrderStatusWidget: Widget {
let kind: String = "OrderStatusWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: OrderProvider()) { entry in
OrderWidgetView(entry: entry)
}
.configurationDisplayName("Order Status")
.description("Track your current order.")
.supportedFamilies([.systemSmall, .systemMedium])
}
}
WidgetBundle
使用 WidgetBundle 从单个扩展公开多个小组件。
@main
struct MyAppWidgets: WidgetBundle {
var body: some Widget {
OrderStatusWidget()
FavoritesWidget()
DeliveryActivityWidget() // ActivityConfiguration 交接
QuickActionControl() // 控制中心
}
}
配置类型
对于不可配置的小组件,使用 StaticConfiguration。对于与 AppIntentTimelineProvider 配对的可配置小组件,推荐使用 AppIntentConfiguration。
// 静态
StaticConfiguration(kind: "MyWidget", provider: MyProvider()) { entry in
MyWidgetView(entry: entry)
}
// 可配置
AppIntentConfiguration(kind: "ConfigWidget", intent: SelectCategoryIntent.self,
provider: CategoryProvider()) { entry in
CategoryWidgetView(entry: entry)
}
共享修饰符
| 修饰符 | 用途 |
|---|---|
.configurationDisplayName(_:) |
小组件库中显示的名称 |
.description(_:) |
小组件库中显示的描述 |
.supportedFamilies(_:) |
WidgetFamily 值数组 |
.supplementalActivityFamilies(_:) |
Live Activity 尺寸(.small、.medium) |
TimelineProvider
用于静态(不可配置)小组件。使用完成处理程序。三个必需方法:
struct WeatherProvider: TimelineProvider {
typealias Entry = WeatherEntry
func placeholder(in context: Context) -> WeatherEntry {
WeatherEntry(date: .now, temperature: 72, condition: "Sunny")
}
func getSnapshot(in context: Context, completion: @escaping (WeatherEntry) -> Void) {
let entry = context.isPreview
? placeholder(in: context)
: WeatherEntry(date: .now, temperature: currentTemp, condition: currentCondition)
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<WeatherEntry>) -> Void) {
Task {
let weather = await WeatherService.shared.fetch()
let entry = WeatherEntry(date: .now, temperature: weather.temp, condition: weather.condition)
let nextUpdate = Calendar.current.date(byAdding: .hour, value: 1, to: .now)!
completion(Timeline(entries: [entry], policy: .after(nextUpdate)))
}
}
}
AppIntentTimelineProvider
用于可配置小组件。原生支持 async/await。接收用户意图配置。
struct CategoryProvider: AppIntentTimelineProvider {
typealias Entry = CategoryEntry
typealias Intent = SelectCategoryIntent
func placeholder(in context: Context) -> CategoryEntry {
CategoryEntry(date: .now, categoryName: "Sample", items: [])
}
func snapshot(for config: SelectCategoryIntent, in context: Context) async -> CategoryEntry {
let items = await DataStore.shared.items(for: config.category)
return CategoryEntry(date: .now, categoryName: config.category.name, items: items)
}
func timeline(for config: SelectCategoryIntent, in context: Context) async -> Timeline<CategoryEntry> {
let items = await DataStore.shared.items(for: config.category)
let entry = CategoryEntry(date: .now, categoryName: config.category.name, items: items)
return Timeline(entries: [entry], policy: .atEnd)
}
}
Widget 家族
| 家族 | 平台 |
|---|---|
.systemSmall |
iOS、iPadOS、macOS、CarPlay (iOS 26+) |
.systemMedium |
iOS、iPadOS、macOS |
.systemLarge |
iOS、iPadOS、macOS |
.systemExtraLarge |
仅 iPadOS |
.accessoryCircular |
iOS、watchOS |
.accessoryRectangular |
iOS、watchOS |
.accessoryInline |
iOS、watchOS |
.accessoryCorner |
仅 watchOS |
使用 @Environment(\.widgetFamily) 按家族调整布局:
@Environment(\.widgetFamily) var family
var body: some View {
switch family {
case .systemSmall: CompactView(entry: entry)
case .systemMedium: DetailedView(entry: entry)
case .accessoryCircular: CircularView(entry: entry)
default: FullView(entry: entry)
}
}
交互式小组件 (iOS 17+)
使用 Button 和 Toggle,配合小组件扩展或共享代码可用的意图类型。WidgetKit 负责视图放置;app-intents 负责意图建模和行为。
struct InteractiveWidgetView: View {
let entry: FavoriteEntry
var body: some View {
Button(intent: ToggleFavoriteIntent(itemID: entry.itemID)) {
Image(systemName: entry.isFavorite ? "star.fill" : "star")
}
}
}
ActivityConfiguration 交接
WidgetKit 在小组件扩展中注册 Live Activity 界面。将此部分保留为注册和渲染交接;使用 activitykit 处理 ActivityAttributes、生命周期、推送更新和完整的 Dynamic Island 模式。
struct DeliveryActivityWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: DeliveryAttributes.self) { context in
DeliveryLiveActivityView(context: context)
} dynamicIsland: { context in
DeliveryDynamicIsland(context: context)
}
}
}
控制中心小组件 (iOS 18+)
WidgetKit 负责控件配置、放置、kind、显示名称、推送处理程序和扩展注册。控制操作和值意图属于 app-intents。
struct OpenCameraControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: "OpenCamera") {
ControlWidgetButton(action: OpenCameraIntent()) {
Label("Camera", systemImage: "camera.fill")
}
}
.displayName("Open Camera")
}
}
struct FlashlightControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: "Flashlight", provider: FlashlightValueProvider()) { value in
ControlWidgetToggle(isOn: value, action: ToggleFlashlightIntent()) {
Label("Flashlight", systemImage: value ? "flashlight.on.fill" : "flashlight.off.fill")
}
}
.displayName("Flashlight")
}
}
锁屏小组件
使用 accessory 家族和 AccessoryWidgetBackground。
struct StepsWidget: Widget {
let kind = "StepsWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: StepsProvider()) { entry in
ZStack {
AccessoryWidgetBackground()
VStack {
Image(systemName: "figure.walk")
Text("\(entry.stepCount)").font(.headline)
}
}
}
.supportedFamilies([.accessoryCircular, .accessoryRectangular, .accessoryInline])
}
}
待机模式
小型系统小组件可以出现在待机和 CarPlay 中。使用 @Environment(\.widgetLocation) 进行条件渲染:
@Environment(\.widgetLocation) var location
// location == .standBy, .homeScreen, .lockScreen, .carPlay, etc.
Widget URL 处理和深度链接
使用一个 .widgetURL(_:) 作为整个小组件的回退路由。仅在家族和布局支持的情况下,在 .accessoryRectangular、.systemSmall 和更大的系统小组件中使用 Link 进行有意的子目标。对于小型小组件,优先使用一个明确的回退;除非视觉提示和点击区域保持明确,否则避免多个 Link 目标。
切勿在层次结构中附加多个 widgetURL 修饰符。
智能叠放相关性
在时间线条目上使用 TimelineEntryRelevance(score:duration:) 以实现及时的 iPhone 和 iPad 智能叠放相关性。保持分数在一致的正数范围内;零或更低表示不相关。
对于可配置小组件,从应用端代码捐赠与用户操作或小组件参数对应的 App Intents,例如使用 intent.donate() 或 IntentDonationManager。将 AppEntity 和 EntityQuery 设计保留在 app-intents 中。
在 watchOS 上,上下文相关性使用来自提供者 relevance() 回调的 WidgetRelevance([WidgetRelevanceAttribute(...)])。该路径不用于 iPhone 或 iPad 智能叠放。
设计模式
- 优先使用
Gauge而非手动弧线。 对于锁屏圆形小组件使用.gaugeStyle(.accessoryCircular),对于主屏幕容量条使用.linearCapacity。系统处理样式、无障碍和渲染模式适配。 - 使用
.containerBackground(_:for: .widget)(iOS 17+) 作为小组件背景,而不是使用 padding 和背景修饰符。 - 使用
Canvas进行密集可视化,如迷你折线图或迷你条形图。由于整个小组件表面是一个点击目标,缺少逐元素无障碍是可以接受的。 - 将时间线刷新与数据粒度匹配。 预算是动态且机会主义的;安排有用的未来条目,避免不必要的重新加载,并使用
Text(timerInterval:countsDown:)进行实时倒计时。加载高级参考以获取当前预算指导。
有关每个模式的代码示例和详细指导,请参阅 references/widgetkit-advanced.md。
iOS 26 新增功能
Liquid Glass 支持
使用 @Environment(\.widgetRenderingMode)、.widgetAccentable() 和 Image.widgetAccentedRenderingMode(_:) 使小组件适应 Liquid Glass。在 .vibrant 模式下,系统将内容映射到材质样式,因此避免仅依赖原始颜色。
推送重新加载处理程序
小组件推送重新加载:
- 为小组件扩展目标添加 Push Notifications 功能。
- 将
WidgetPushHandler类型保留在小组件扩展目标或链接到其中的共享代码中,而不仅仅在主应用目标中。 - 使用
.pushHandler(...)在小组件配置上注册处理程序。 - 不要使用 User Notifications 注册来获取小组件推送令牌;WidgetKit 通过
pushTokenDidChange(_:widgets:)提供令牌。 - 使用
apns-push-type: widgets、主题后缀.push-type.widgets和aps.content-changed。 - 将推送视为有预算的、机会主义的重新加载信号,而不是状态传递,也不是唯一的更新模型。时间线、重新加载策略、共享存储或重新获取,以及应用触发的
WidgetCenter重新加载仍然是回退路径。
控制推送重新加载:
- 使用
.pushHandler(...)在ControlWidgetConfiguration上注册ControlPushHandler。 pushTokensDidChange(controls:)接收[ControlInfo];从每个控件的pushInfo读取令牌。- 使用
apns-push-type: controls、主题后缀.push-type.controls和aps.content-changed。
CarPlay 小组件
小型系统小组件可以在 iOS 26+ 的 CarPlay 中显示。确保布局一目了然;点击和控件取决于车辆触摸支持,并且对于打开应用,取决于 CarPlay 集成。
常见错误
-
使用 IntentTimelineProvider 而不是 AppIntentTimelineProvider。
IntentTimelineProvider是较旧的 SiriKit Intents 提供者。对于新小组件,优先使用AppIntentTimelineProvider和 App Intents 框架。 -
超出刷新预算。 小组件有每日刷新限制。不要在每个微小数据更改时调用
WidgetCenter.shared.reloadTimelines(ofKind:)。批量更新并使用适当的TimelineReloadPolicy值。 -
忘记为共享数据使用 App Groups。 小组件扩展在单独的进程中运行。使用
UserDefaults(suiteName:)或共享的 App Group 容器来存储小组件读取的数据。 -
在 placeholder() 中执行网络调用。
placeholder(in:)必须使用示例数据同步返回。使用getTimeline或timeline(for:in:)进行异步工作。 -
将 WidgetKit 推送负载视为状态。 小组件和控制推送是重新加载信号。在共享存储中持久化状态,或在提供者中重新获取。
-
通过 User Notifications 注册小组件推送。 小组件推送令牌来自 WidgetKit 处理程序,而不是
UNUserNotificationCenter。 -
在小组件视图中放置繁重逻辑。 小组件视图在大小受限的进程中渲染。在时间线提供者中预计算数据,并通过条目传递可直接显示的值。
-
忽略 accessory 渲染模式。 锁屏小组件以
.vibrant或.accented模式渲染,而不是.fullColor。使用@Environment(\.widgetRenderingMode)进行测试,避免仅依赖颜色。 -
未在设备上测试。 待机、CarPlay 和 accessory 渲染与模拟器差异很大。始终在物理硬件上验证。
审查清单
- [ ] 小组件扩展目标具有与主应用匹配的 App Groups 授权
- [ ]
@main位于WidgetBundle上,而不是单个小组件上 - [ ]
placeholder(in:)同步返回;getSnapshot/snapshot(for:in:)在isPreview时快速 - [ ] 时间线重新加载策略与更新频率匹配;仅在数据更改时使用
reloadTimelines(ofKind:) - [ ] 布局根据
WidgetFamily调整;accessory 小组件在.vibrant模式下测试 - [ ] 交互式小组件使用扩展可用的 App Intents,仅使用
Button/Toggle - [ ] 使用一个
.widgetURL(_:)回退;Link子目标适合家族 - [ ] 小组件推送处理程序位于小组件扩展/共享代码中,不使用 User Notifications 令牌注册
- [ ] 小组件/控制推送补充时间线和共享状态/重新获取回退
- [ ] 智能叠放相关性使用时间线相关性和应用端意图捐赠(如有用)
- [ ] Live Activity 生命周期和 App Intent 建模交给同级技能
- [ ] 控件使用
StaticControlConfiguration/AppIntentControlConfiguration - [ ] 时间线条目和 Intent 类型是 Sendable 的;在设备上测试
参考资料
- 高级指南:references/widgetkit-advanced.md
- Apple 文档:WidgetKit | 保持小组件更新 | 智能叠放可见性






