关于 widgetkit
widgetkit 是面向 iOS 开发者的 WidgetKit 技能,指导构建主屏幕、锁定屏幕、控制中心及其他 WidgetKit 界面,并涵盖配置、时间线和交互设计。
主要功能
- 01构建多类 WidgetKit 界面
- 02配置静态与可配置小组件
- 03实现时间线数据提供器
- 04适配不同小组件尺寸
- 05添加交互及控制中心控件
- 06指导深层链接与相关性设置
适用场景
- 01开发主屏幕或锁定屏幕小组件
- 02为控制中心添加快捷控件
- 03排查时间线刷新与布局问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill widgetkit安装后在 Claude Code 中输入/widgetkit确认已加载。
附带文件evals/evals.jsonreferences/widgetkit-advanced.md
详细介绍
WidgetKit
为 iOS 26+ 构建主屏幕小组件、锁屏小组件、控制中心控件,以及待机模式或 CarPlay 小组件界面。
将相邻框架的指导范围限定为与 WidgetKit 的集成。仅在 ActivityKit 和 App Intents 与 WidgetKit 界面直接相关时才涉及;完整的生命周期、APNs 内容状态、Siri/快捷指令/Spotlight 或实体建模工作请交由同级的 activitykit 或 app-intents 技能处理。
时间线策略、推送更新、Xcode 设置和高级模式请参阅 references/widgetkit-advanced.md。
目录
- 工作流
- Widget 协议和 WidgetBundle
- 配置类型
- TimelineProvider
- AppIntentTimelineProvider
- 小组件族
- 交互式小组件(iOS 17+)
- ActivityConfiguration 交接
- 控制中心小组件(iOS 18+)
- 锁屏小组件
- 待机模式
- 小组件 URL 处理和深层链接
- 智能堆叠相关性
- 设计模式
- iOS 26 新增内容
- 常见错误
- 审核清单
- 参考资料
工作流
1. 创建新小组件
- 在 Xcode 中添加一个 Widget Extension 目标(File > New > Target > Widget Extension)。
- 为 App Groups 启用共享数据,以便 App 和小组件扩展共用数据。
- 定义一个包含
date属性和显示数据的TimelineEntry结构体。 - 实现
TimelineProvider(静态)或AppIntentTimelineProvider(可配置)。 - 使用 SwiftUI 构建小组件视图,并根据
WidgetFamily调整布局。 - 声明一个遵循
Widget的结构体,并配置小组件及其支持的族。 - 使用
@main注解的WidgetBundle注册所有小组件。
2. 集成相邻界面
- 当 App 包含实时活动时,在小组件包中注册
ActivityConfiguration;但ActivityAttributes、请求/更新/结束、APNscontent-state和灵动岛布局的详细实现请交由activitykit处理。 - 在 WidgetKit 视图或控件中使用
Button、Toggle、ControlWidgetButton和ControlWidgetToggle;但意图建模、实体、查询、Siri、快捷指令和 Spotlight 请交由app-intents处理。
3. 添加控制中心控件
- 为按钮复用一个
AppIntent/OpenIntent,或为开关复用一个SetValueIntent。 - 在小组件包中创建
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 handoff
QuickActionControl() // Control Center
}
}
配置类型
对于不可配置的小组件,使用 StaticConfiguration。对于与 AppIntentTimelineProvider 配对的可配置小组件,使用 AppIntentConfiguration(推荐)。
// Static
StaticConfiguration(kind: "MyWidget", provider: MyProvider()) { entry in
MyWidgetView(entry: entry)
}
// Configurable
AppIntentConfiguration(kind: "ConfigWidget", intent: SelectCategoryIntent.self,
provider: CategoryProvider()) { entry in
CategoryWidgetView(entry: entry)
}
共享修饰符
| 修饰符 | 用途 |
|---|---|
.configurationDisplayName(_:) | 在小组件图库中显示的名称 |
.description(_:) | 在小组件图库中显示的描述 |
.supportedFamilies(_:) | WidgetFamily 值数组 |
.supplementalActivityFamilies(_:) | 实时活动尺寸(.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)
}
}
交互式 Widget(iOS 17+)
将 Button 和 Toggle 与 Widget 扩展或共享代码可用的意图类型结合使用。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 在 Widget 扩展中注册实时活动界面。本节仅介绍注册和渲染交接;ActivityAttributes、生命周期、推送更新和完整的 Dynamic Island 模式请参阅 activitykit。
struct DeliveryActivityWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: DeliveryAttributes.self) { context in
DeliveryLiveActivityView(context: context)
} dynamicIsland: { context in
DeliveryDynamicIsland(context: context)
}
}
}
控制中心 Widget(iOS 18+)
WidgetKit 负责控制项配置、布局、类型、显示名称、推送处理程序和扩展注册。控制操作和值意图属于 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")
}
}
锁定屏幕 Widget
使用配件系列和 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])
}
}
待机模式
小型系统 Widget 可以显示在待机模式和 CarPlay 中。使用 @Environment(\.widgetLocation) 进行条件渲染:
@Environment(\.widgetLocation) var location
// location == .standBy, .homeScreen, .lockScreen, .carPlay, etc.
Widget URL 处理和深层链接
使用一个 .widgetURL(_:) 作为整个 Widget 的备用路由。仅在系列和布局支持时,才使用 Link 创建明确的子目标,包括 .accessoryRectangular、.systemSmall 和更大的系统 Widget。对于小型 Widget,优先使用一个清晰的备用路由;除非视觉提示和点击区域仍然明确,否则应避免设置多个 Link 目标。
切勿在层级结构中附加多个 widgetURL 修饰符。
Smart Stack 相关性
在时间线条目上使用 TimelineEntryRelevance(score:duration:),以提升 iPhone 和 iPad Smart Stack 的时效性相关性。分数应采用一致的正数尺度;零或负数表示不相关。
对于可配置的 Widget,请从应用端代码捐赠与用户操作或 Widget 参数相对应的 App Intents,例如使用 intent.donate() 或 IntentDonationManager。AppEntity 和 EntityQuery 的设计请参阅 app-intents。
在 watchOS 上,上下文相关性通过提供程序 relevance() 回调中的 WidgetRelevance([WidgetRelevanceAttribute(...)]) 实现。iPhone 或 iPad Smart Stack 不使用此路径。
设计模式
- 优先使用
Gauge,而不是手动绘制弧线。 锁定屏幕圆形 Widget 使用.gaugeStyle(.accessoryCircular),主屏幕容量条使用.linearCapacity。系统会处理样式、辅助功能和渲染模式适配。 - 使用
.containerBackground(_:for: .widget)(iOS 17+)设置 Widget 背景,而不是使用内边距和背景修饰符。 - 使用
Canvas绘制密集型可视化内容,例如迷你趋势图或迷你条形图。由于整个 Widget 界面是单一点击目标,因此可以接受无法单独为各元素提供辅助功能支持。 - 使时间线刷新频率与数据粒度相匹配。 刷新预算是动态且机会性的;请安排有用的未来条目,避免不必要的重新加载,并使用
Text(timerInterval:countsDown:)显示实时倒计时。请查阅进阶参考,了解当前的预算指导。
请参阅 references/widgetkit-advanced.md,了解各模式的代码示例和详细指导。
iOS 26 新增内容
Liquid Glass 支持
使用 @Environment(\.widgetRenderingMode)、.widgetAccentable() 和 Image.widgetAccentedRenderingMode(_:),让 Widget 适配 Liquid Glass。在 .vibrant 模式下,系统会将内容映射为材质样式,因此不要只依赖原始颜色。
推送重新加载处理程序
小组件推送重新加载:
- 为小组件扩展目标添加推送通知功能。
- 将
WidgetPushHandler类型放在小组件扩展目标中,或放在链接到该目标的共享代码中,而不要只放在主应用目标中。 - 在小组件配置上使用
.pushHandler(...)注册处理程序。 - 不要通过注册用户通知来获取小组件推送令牌;WidgetKit 会通过
pushTokenDidChange(_:widgets:)提供令牌。 - 使用
apns-push-type: widgets、主题后缀.push-type.widgets和aps.content-changed。 - 将推送视为受预算限制、机会式的重新加载信号,而不是状态传递机制,也不要将其作为唯一的新鲜度保障方式。时间线、重新加载策略、共享存储或重新获取数据,以及由应用触发的
WidgetCenter重新加载,仍是备用途径。
控制项推送重新加载:
- 在
ControlWidgetConfiguration上使用.pushHandler(...)注册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 的提供程序。对于新小组件,优先使用搭配 App Intents 框架的AppIntentTimelineProvider。 -
超出刷新预算。 小组件有每日刷新次数限制。不要在每次细微的数据变更时都调用
WidgetCenter.shared.reloadTimelines(ofKind:)。请批量处理更新,并使用恰当的TimelineReloadPolicy值。 -
忘记为共享数据启用 App Groups。 小组件扩展在独立进程中运行。对于小组件要读取的数据,请使用
UserDefaults(suiteName:)或共享的 App Group 容器。 -
在 placeholder() 中执行网络请求。
placeholder(in:)必须同步返回示例数据。异步工作请使用getTimeline或timeline(for:in:)。 -
将 WidgetKit 推送负载视为状态。 小组件和控制项推送是重新加载信号。请将状态持久化到共享存储中,或在提供程序中重新获取状态。
-
通过用户通知注册小组件推送。 小组件推送令牌来自 WidgetKit 处理程序,而非
UNUserNotificationCenter。 -
在小组件视图中放入繁重逻辑。 小组件视图在受限进程中渲染。请在时间线提供程序中预先计算数据,并通过条目传入可直接显示的值。
-
忽略配件渲染模式。 锁定屏幕小组件以
.vibrant或.accented模式渲染,而非.fullColor。请使用@Environment(\.widgetRenderingMode)进行测试,并避免仅依赖颜色来传达信息。 -
不在设备上测试。 待机模式、CarPlay 和配件渲染与模拟器有显著差异。请务必在实体设备上验证。
审查清单
- 小组件扩展目标具有与主应用匹配的 App Groups 权限
-
@main标注在WidgetBundle上,而不是单个小组件上 -
placeholder(in:)同步返回;当isPreview为真时,getSnapshot/snapshot(for:in:)快速返回 - 时间线重新加载策略与更新频率匹配;仅在数据变化时调用
reloadTimelines(ofKind:) - 布局会针对每种
WidgetFamily进行适配;在.vibrant模式下测试配件小组件 - 交互式小组件仅使用扩展中可用的 App Intents,配合
Button/Toggle - 使用一个
.widgetURL(_:)作为回退;Link子目标适用于相应的小组件类型 - 小组件推送处理程序位于小组件扩展/共享代码中,且不使用 User Notifications 令牌注册
- 小组件/控制项推送作为时间线和共享状态/重新获取回退机制的补充
- 在适用时,使用时间线相关性和应用端意图捐赠来设置 Smart Stack 相关性
- Live Activity 生命周期和 App Intent 建模交由相关技能处理
- 控制项使用
StaticControlConfiguration/AppIntentControlConfiguration - 时间线条目和 Intent 类型遵循 Sendable;已在设备上测试
参考资料
- 进阶指南:references/widgetkit-advanced.md
- Apple 文档:WidgetKit | 使小组件保持最新 | Smart Stack 可见性