mcpField
W
widgetkit

widgetkit

github.com/dpearson2699开发1.2k4.0k 次安装13 周前更新
代码仓库

关于 widgetkit

widgetkit 是面向 iOS 开发者的 WidgetKit 技能,指导构建主屏幕、锁定屏幕、控制中心及其他 WidgetKit 界面,并涵盖配置、时间线和交互设计。

主要功能

  1. 01构建多类 WidgetKit 界面
  2. 02配置静态与可配置小组件
  3. 03实现时间线数据提供器
  4. 04适配不同小组件尺寸
  5. 05添加交互及控制中心控件
  6. 06指导深层链接与相关性设置

适用场景

  1. 01开发主屏幕或锁定屏幕小组件
  2. 02为控制中心添加快捷控件
  3. 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。

目录

工作流

1. 创建新小组件
  1. 在 Xcode 中添加一个 Widget Extension 目标(File > New > Target > Widget Extension)。
  2. 为 App Groups 启用共享数据,以便 App 和小组件扩展共用数据。
  3. 定义一个包含 date 属性和显示数据的 TimelineEntry 结构体。
  4. 实现 TimelineProvider(静态)或 AppIntentTimelineProvider(可配置)。
  5. 使用 SwiftUI 构建小组件视图,并根据 WidgetFamily 调整布局。
  6. 声明一个遵循 Widget 的结构体,并配置小组件及其支持的族。
  7. 使用 @main 注解的 WidgetBundle 注册所有小组件。
2. 集成相邻界面
  1. 当 App 包含实时活动时,在小组件包中注册 ActivityConfiguration;但 ActivityAttributes、请求/更新/结束、APNs content-state 和灵动岛布局的详细实现请交由 activitykit 处理。
  2. 在 WidgetKit 视图或控件中使用 Button、Toggle、ControlWidgetButton 和 ControlWidgetToggle;但意图建模、实体、查询、Siri、快捷指令和 Spotlight 请交由 app-intents 处理。
3. 添加控制中心控件
  1. 为按钮复用一个 AppIntent/OpenIntent,或为开关复用一个 SetValueIntent。
  2. 在小组件包中创建 ControlWidgetButton 或 ControlWidgetToggle。
  3. 使用 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 系列

系列平台
.systemSmalliOS、iPadOS、macOS、CarPlay(iOS 26+)
.systemMediumiOS、iPadOS、macOS
.systemLargeiOS、iPadOS、macOS
.systemExtraLarge仅限 iPadOS
.accessoryCirculariOS、watchOS
.accessoryRectangulariOS、watchOS
.accessoryInlineiOS、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 集成。

常见错误

  1. 使用 IntentTimelineProvider 而非 AppIntentTimelineProvider。 IntentTimelineProvider 是较早基于 SiriKit Intents 的提供程序。对于新小组件,优先使用搭配 App Intents 框架的 AppIntentTimelineProvider。

  2. 超出刷新预算。 小组件有每日刷新次数限制。不要在每次细微的数据变更时都调用 WidgetCenter.shared.reloadTimelines(ofKind:)。请批量处理更新,并使用恰当的 TimelineReloadPolicy 值。

  3. 忘记为共享数据启用 App Groups。 小组件扩展在独立进程中运行。对于小组件要读取的数据,请使用 UserDefaults(suiteName:) 或共享的 App Group 容器。

  4. 在 placeholder() 中执行网络请求。 placeholder(in:) 必须同步返回示例数据。异步工作请使用 getTimeline 或 timeline(for:in:)。

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

  6. 通过用户通知注册小组件推送。 小组件推送令牌来自 WidgetKit 处理程序,而非 UNUserNotificationCenter。

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

  8. 忽略配件渲染模式。 锁定屏幕小组件以 .vibrant 或 .accented 模式渲染,而非 .fullColor。请使用 @Environment(\.widgetRenderingMode) 进行测试,并避免仅依赖颜色来传达信息。

  9. 不在设备上测试。 待机模式、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;已在设备上测试

参考资料

评论

支持 Markdown