widgetkit

widgetkit

热门

实现、审查或改进 WidgetKit 小组件和控件。适用于构建主屏幕、锁屏、待机或 CarPlay 小组件(使用时间线提供者);可配置小组件(使用 AppIntentTimelineProvider);交互式小组件或控制中心控件(使用 Button/Toggle 连接);WidgetKit 推送刷新、刷新预算、深度链接、智能叠放相关性、Liquid Glass/强调色渲染、小组件扩展设置、WidgetBundle、App Groups 和授权。

932Star
47Fork
更新于 2026/7/15
SKILL.md
readonly只读
name
widgetkit
description

实现、审查或改进 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 或实体建模工作交给同级的 activitykitapp-intents 技能。

有关时间线策略、基于推送的更新、Xcode 设置和高级模式,请参阅 references/widgetkit-advanced.md

目录

工作流程

1. 创建新小组件

  1. 在 Xcode 中添加 Widget Extension 目标(File > New > Target > Widget Extension)。
  2. 启用 App Groups 以在应用和小组件扩展之间共享数据。
  3. 定义 TimelineEntry 结构体,包含 date 属性和显示数据。
  4. 实现 TimelineProvider(静态)或 AppIntentTimelineProvider(可配置)。
  5. 使用 SwiftUI 构建小组件视图,根据 WidgetFamily 调整布局。
  6. 声明符合 Widget 协议的结构体,包含配置和支持的家族。
  7. 在带有 @main 注解的 WidgetBundle 中注册所有小组件。

2. 集成相邻界面

  1. 当应用有 Live Activity 时,在 widget bundle 中注册 ActivityConfiguration,但将 ActivityAttributes、请求/更新/结束、APNs content-state 和 Dynamic Island 布局深度保留在 activitykit 中。
  2. 在 WidgetKit 视图或控件中放置 ButtonToggleControlWidgetButtonControlWidgetToggle,但将意图建模、实体、查询、Siri、Shortcuts 和 Spotlight 保留在 app-intents 中。

3. 添加控制中心控件

  1. 为按钮复用 AppIntent/OpenIntent,或为开关复用 SetValueIntent
  2. 在 widget bundle 中创建 ControlWidgetButtonControlWidgetToggle
  3. 使用 StaticControlConfigurationAppIntentControlConfiguration

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+)

使用 ButtonToggle,配合小组件扩展或共享代码可用的意图类型。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。将 AppEntityEntityQuery 设计保留在 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.widgetsaps.content-changed
  • 将推送视为有预算的、机会主义的重新加载信号,而不是状态传递,也不是唯一的更新模型。时间线、重新加载策略、共享存储或重新获取,以及应用触发的 WidgetCenter 重新加载仍然是回退路径。

控制推送重新加载:

  • 使用 .pushHandler(...)ControlWidgetConfiguration 上注册 ControlPushHandler
  • pushTokensDidChange(controls:) 接收 [ControlInfo];从每个控件的 pushInfo 读取令牌。
  • 使用 apns-push-type: controls、主题后缀 .push-type.controlsaps.content-changed

CarPlay 小组件

小型系统小组件可以在 iOS 26+ 的 CarPlay 中显示。确保布局一目了然;点击和控件取决于车辆触摸支持,并且对于打开应用,取决于 CarPlay 集成。

常见错误

  1. 使用 IntentTimelineProvider 而不是 AppIntentTimelineProvider。
    IntentTimelineProvider 是较旧的 SiriKit Intents 提供者。对于新小组件,优先使用 AppIntentTimelineProvider 和 App Intents 框架。

  2. 超出刷新预算。 小组件有每日刷新限制。不要在每个微小数据更改时调用 WidgetCenter.shared.reloadTimelines(ofKind:)。批量更新并使用适当的 TimelineReloadPolicy 值。

  3. 忘记为共享数据使用 App Groups。 小组件扩展在单独的进程中运行。使用 UserDefaults(suiteName:) 或共享的 App Group 容器来存储小组件读取的数据。

  4. 在 placeholder() 中执行网络调用。 placeholder(in:) 必须使用示例数据同步返回。使用 getTimelinetimeline(for:in:) 进行异步工作。

  5. 将 WidgetKit 推送负载视为状态。 小组件和控制推送是重新加载信号。在共享存储中持久化状态,或在提供者中重新获取。

  6. 通过 User Notifications 注册小组件推送。 小组件推送令牌来自 WidgetKit 处理程序,而不是 UNUserNotificationCenter

  7. 在小组件视图中放置繁重逻辑。 小组件视图在大小受限的进程中渲染。在时间线提供者中预计算数据,并通过条目传递可直接显示的值。

  8. 忽略 accessory 渲染模式。 锁屏小组件以 .vibrant.accented 模式渲染,而不是 .fullColor。使用 @Environment(\.widgetRenderingMode) 进行测试,避免仅依赖颜色。

  9. 未在设备上测试。 待机、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 的;在设备上测试

参考资料