widgetkit by dpearson2699/swift-ios-skills
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill widgetkit为 iOS 26+ 构建主屏幕小组件、锁定屏幕小组件、实时活动、灵动岛呈现、控制中心控件以及待机模式界面。
关于时间线策略、基于推送的更新、Xcode 设置和高级模式,请参阅 references/widgetkit-advanced.md。
date 属性和显示数据的 TimelineEntry 结构体。TimelineProvider(静态)或 AppIntentTimelineProvider(可配置)。广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
WidgetFamily 调整布局。Widget 协议的结构体,包含配置和支持的系列。@main 注解的 WidgetBundle 中注册所有小组件。ContentState 的 ActivityAttributes 结构体。NSSupportsLiveActivities = YES。ActivityConfiguration,包含锁定屏幕内容和灵动岛闭包。Activity.request(attributes:content:pushType:) 启动活动。activity.update(_:) 进行更新,使用 activity.end(_:dismissalPolicy:) 结束活动。AppIntent。ControlWidgetButton 或 ControlWidgetToggle。StaticControlConfiguration 或 AppIntentControlConfiguration。通读本文档末尾的审查清单。
每个小组件都符合 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("订单状态")
.description("跟踪您当前的订单。")
.supportedFamilies([.systemSmall, .systemMedium])
}
}
使用 WidgetBundle 从单个扩展中暴露多个小组件。
@main
struct MyAppWidgets: WidgetBundle {
var body: some Widget {
OrderStatusWidget()
FavoritesWidget()
DeliveryActivityWidget() // 实时活动
QuickActionControl() // 控制中心
}
}
对于不可配置的小组件,使用 StaticConfiguration。对于可配置的小组件,使用 AppIntentConfiguration(推荐)并与 AppIntentTimelineProvider 配对使用。
// 静态
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(_:) | 实时活动尺寸 (.small, .medium) |
用于静态(不可配置)小组件。使用完成处理程序。需要三个方法:
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)))
}
}
}
用于可配置的小组件。原生使用 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)
}
}
| 系列 | 平台 |
|---|---|
.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)
}
}
使用带有符合 AppIntent 协议的类型的 Button 和 Toggle,可以直接从小组件执行操作而无需启动应用。
struct ToggleFavoriteIntent: AppIntent {
static var title: LocalizedStringResource = "切换收藏"
@Parameter(title: "项目 ID") var itemID: String
func perform() async throws -> some IntentResult {
await DataStore.shared.toggleFavorite(itemID)
return .result()
}
}
struct InteractiveWidgetView: View {
let entry: FavoriteEntry
var body: some View {
HStack {
Text(entry.itemName)
Spacer()
Button(intent: ToggleFavoriteIntent(itemID: entry.itemID)) {
Image(systemName: entry.isFavorite ? "star.fill" : "star")
}
}
.padding()
}
}
定义静态和动态数据模型。
struct DeliveryAttributes: ActivityAttributes {
struct ContentState: Codable, Hashable {
var driverName: String
var estimatedDeliveryTime: ClosedRange<Date>
var currentStep: DeliveryStep
}
var orderNumber: Int
var restaurantName: String
}
在小组件包中提供锁定屏幕内容和灵动岛闭包。
struct DeliveryActivityWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: DeliveryAttributes.self) { context in
VStack(alignment: .leading) {
Text(context.attributes.restaurantName).font(.headline)
HStack {
Text("司机: \(context.state.driverName)")
Spacer()
Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true)
}
}
.padding()
} dynamicIsland: { context in
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
Image(systemName: "box.truck.fill").font(.title2)
}
DynamicIslandExpandedRegion(.trailing) {
Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true)
.font(.caption)
}
DynamicIslandExpandedRegion(.center) {
Text(context.attributes.restaurantName).font(.headline)
}
DynamicIslandExpandedRegion(.bottom) {
HStack {
ForEach(DeliveryStep.allCases, id: \.self) { step in
Image(systemName: step.icon)
.foregroundStyle(step <= context.state.currentStep ? .primary : .tertiary)
}
}
}
} compactLeading: {
Image(systemName: "box.truck.fill")
} compactTrailing: {
Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true)
.frame(width: 40).monospacedDigit()
} minimal: {
Image(systemName: "box.truck.fill")
}
}
}
}
| 区域 | 位置 |
|---|---|
.leading | TrueDepth 摄像头左侧;可换行至下方 |
.trailing | TrueDepth 摄像头右侧;可换行至下方 |
.center | 摄像头正下方 |
.bottom | 所有其他区域下方 |
// 启动
let attributes = DeliveryAttributes(orderNumber: 123, restaurantName: "Pizza Place")
let state = DeliveryAttributes.ContentState(
driverName: "Alex",
estimatedDeliveryTime: Date()...Date().addingTimeInterval(1800),
currentStep: .preparing
)
let content = ActivityContent(state: state, staleDate: nil, relevanceScore: 75)
let activity = try Activity.request(attributes: attributes, content: content, pushType: .token)
// 更新(可选择附带提醒)
let updated = ActivityContent(state: newState, staleDate: nil, relevanceScore: 90)
await activity.update(updated)
await activity.update(updated, alertConfiguration: AlertConfiguration(
title: "订单更新", body: "您的司机就在附近!", sound: .default
))
// 结束
let final = ActivityContent(state: finalState, staleDate: nil, relevanceScore: 0)
await activity.end(final, dismissalPolicy: .after(.now.addingTimeInterval(3600)))
// 按钮控件
struct OpenCameraControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: "OpenCamera") {
ControlWidgetButton(action: OpenCameraIntent()) {
Label("相机", systemImage: "camera.fill")
}
}
.displayName("打开相机")
}
}
// 带值提供者的切换控件
struct FlashlightControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: "Flashlight", provider: FlashlightValueProvider()) { value in
ControlWidgetToggle(isOn: value, action: ToggleFlashlightIntent()) {
Label("手电筒", systemImage: value ? "flashlight.on.fill" : "flashlight.off.fill")
}
}
.displayName("手电筒")
}
}
使用配件系列和 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])
}
}
.systemSmall 小组件会自动出现在待机模式中(iPhone 在横向充电时)。使用 @Environment(\.widgetLocation) 进行条件渲染:
@Environment(\.widgetLocation) var location
// location == .standBy, .homeScreen, .lockScreen, .carPlay, 等。
使用 WidgetAccentedRenderingMode 使小组件适应 Liquid Glass 视觉风格。
| 模式 | 描述 |
|---|---|
.accented | Liquid Glass 的强调色渲染 |
.accentedDesaturated | 带去饱和度的强调色 |
.desaturated | 完全去饱和 |
.fullColor | 全彩渲染 |
启用基于推送的时间线重新加载,无需计划轮询。
struct MyWidgetPushHandler: WidgetPushHandler {
func pushTokenDidChange(_ pushInfo: WidgetPushInfo, widgets: [WidgetInfo]) {
let tokenString = pushInfo.token.map { String(format: "%02x", $0) }.joined()
// 将 tokenString 发送到您的服务器
}
}
.systemSmall 小组件在 iOS 26+ 的 CarPlay 中渲染。为确保驾驶安全,请确保小组件布局一目了然,清晰易读。
使用 IntentTimelineProvider 而不是 AppIntentTimelineProvider。 IntentTimelineProvider 已弃用。请使用 App Intents 框架的 AppIntentTimelineProvider。
超出刷新预算。 小组件有每日刷新限制。不要在每个次要数据更改时都调用 WidgetCenter.shared.reloadTimelines(ofKind:)。请批量更新并使用适当的 TimelineReloadPolicy 值。
忘记为共享数据设置 App Groups。 小组件扩展在单独的进程中运行。对于小组件读取的数据,请使用 UserDefaults(suiteName:) 或共享的 App Group 容器。
在 placeholder() 中执行网络调用。 placeholder(in:) 必须同步返回示例数据。对于异步工作,请使用 getTimeline 或 timeline(for:in:)。
缺少 NSSupportsLiveActivities Info.plist 键。 如果宿主应用的 Info.plist 中没有 NSSupportsLiveActivities = YES,实时活动将无法启动。
使用已弃用的 contentState API。 对于所有 Activity.request、update 和 end 调用,请使用 ActivityContent。基于 contentState 的方法已弃用。
未处理陈旧状态。 在实时活动视图中检查 context.isStale,并在内容过时时显示回退内容(例如“正在更新...”)。
在小组件视图中放置繁重的逻辑。 小组件视图在大小受限的进程中渲染。请在时间线提供者中预先计算数据,并通过条目传递准备就绪的显示值。
忽略配件渲染模式。 锁定屏幕小组件以 .vibrant 或 .accented 模式渲染,而不是 .fullColor。请使用 @Environment(\.widgetRenderingMode) 进行测试,避免仅依赖颜色。
未在设备上测试。 灵动岛和待机模式的行为与模拟器有显著差异。请务必在物理硬件上验证。
@main 位于 WidgetBundle 上,而不是单个小组件上placeholder(in:) 同步返回;getSnapshot/snapshot(for:in:) 在 isPreview 时快速返回reloadTimelines(ofKind:)WidgetFamily 调整;配件小组件在 .vibrant 模式下测试AppIntent 的 Button/ToggleNSSupportsLiveActivities = YES;使用了 ActivityContent;实现了灵动岛闭包activity.end(_:dismissalPolicy:);控件使用了 StaticControlConfiguration/AppIntentControlConfigurationreferences/widgetkit-advanced.md每周安装量
391
代码仓库
GitHub 星标数
269
首次出现
2026年3月3日
安全审计
安装于
codex386
kimi-cli383
amp383
cline383
github-copilot383
opencode383
Build home screen widgets, Lock Screen widgets, Live Activities, Dynamic Island presentations, Control Center controls, and StandBy surfaces for iOS 26+.
See references/widgetkit-advanced.md for timeline strategies, push-based updates, Xcode setup, and advanced patterns.
TimelineEntry struct with a date property and display data.TimelineProvider (static) or AppIntentTimelineProvider (configurable).WidgetFamily.Widget conforming struct with a configuration and supported families.WidgetBundle annotated with @main.ActivityAttributes struct with a nested ContentState.NSSupportsLiveActivities = YES to the app's Info.plist.ActivityConfiguration in the widget bundle with Lock Screen content and Dynamic Island closures.Activity.request(attributes:content:pushType:).activity.update(_:) and end with activity.end(_:dismissalPolicy:).AppIntent for the action.ControlWidgetButton or ControlWidgetToggle in the widget bundle.StaticControlConfiguration or AppIntentControlConfiguration.Run through the Review Checklist at the end of this document.
Every widget conforms to the Widget protocol and returns a WidgetConfiguration from its body.
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])
}
}
Use WidgetBundle to expose multiple widgets from a single extension.
@main
struct MyAppWidgets: WidgetBundle {
var body: some Widget {
OrderStatusWidget()
FavoritesWidget()
DeliveryActivityWidget() // Live Activity
QuickActionControl() // Control Center
}
}
Use StaticConfiguration for non-configurable widgets. Use AppIntentConfiguration (recommended) for configurable widgets paired with AppIntentTimelineProvider.
// 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)
}
| Modifier | Purpose |
|---|---|
.configurationDisplayName(_:) | Name shown in the widget gallery |
.description(_:) | Description shown in the widget gallery |
.supportedFamilies(_:) | Array of WidgetFamily values |
.supplementalActivityFamilies(_:) | Live Activity sizes (.small, .medium) |
For static (non-configurable) widgets. Uses completion handlers. Three required methods:
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)))
}
}
}
For configurable widgets. Uses async/await natively. Receives user intent configuration.
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)
}
}
| Family | Platform |
|---|---|
.systemSmall | iOS, iPadOS, macOS, CarPlay (iOS 26+) |
.systemMedium | iOS, iPadOS, macOS |
.systemLarge | iOS, iPadOS, macOS |
.systemExtraLarge | iPadOS only |
| Family | Platform |
|---|---|
.accessoryCircular | iOS, watchOS |
.accessoryRectangular | iOS, watchOS |
.accessoryInline | iOS, watchOS |
.accessoryCorner | watchOS only |
Adapt layout per family using @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)
}
}
Use Button and Toggle with AppIntent conforming types to perform actions directly from a widget without launching the app.
struct ToggleFavoriteIntent: AppIntent {
static var title: LocalizedStringResource = "Toggle Favorite"
@Parameter(title: "Item ID") var itemID: String
func perform() async throws -> some IntentResult {
await DataStore.shared.toggleFavorite(itemID)
return .result()
}
}
struct InteractiveWidgetView: View {
let entry: FavoriteEntry
var body: some View {
HStack {
Text(entry.itemName)
Spacer()
Button(intent: ToggleFavoriteIntent(itemID: entry.itemID)) {
Image(systemName: entry.isFavorite ? "star.fill" : "star")
}
}
.padding()
}
}
Define the static and dynamic data model.
struct DeliveryAttributes: ActivityAttributes {
struct ContentState: Codable, Hashable {
var driverName: String
var estimatedDeliveryTime: ClosedRange<Date>
var currentStep: DeliveryStep
}
var orderNumber: Int
var restaurantName: String
}
Provide Lock Screen content and Dynamic Island closures in the widget bundle.
struct DeliveryActivityWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: DeliveryAttributes.self) { context in
VStack(alignment: .leading) {
Text(context.attributes.restaurantName).font(.headline)
HStack {
Text("Driver: \(context.state.driverName)")
Spacer()
Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true)
}
}
.padding()
} dynamicIsland: { context in
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
Image(systemName: "box.truck.fill").font(.title2)
}
DynamicIslandExpandedRegion(.trailing) {
Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true)
.font(.caption)
}
DynamicIslandExpandedRegion(.center) {
Text(context.attributes.restaurantName).font(.headline)
}
DynamicIslandExpandedRegion(.bottom) {
HStack {
ForEach(DeliveryStep.allCases, id: \.self) { step in
Image(systemName: step.icon)
.foregroundStyle(step <= context.state.currentStep ? .primary : .tertiary)
}
}
}
} compactLeading: {
Image(systemName: "box.truck.fill")
} compactTrailing: {
Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true)
.frame(width: 40).monospacedDigit()
} minimal: {
Image(systemName: "box.truck.fill")
}
}
}
}
| Region | Position |
|---|---|
.leading | Left of the TrueDepth camera; wraps below |
.trailing | Right of the TrueDepth camera; wraps below |
.center | Directly below the camera |
.bottom | Below all other regions |
// Start
let attributes = DeliveryAttributes(orderNumber: 123, restaurantName: "Pizza Place")
let state = DeliveryAttributes.ContentState(
driverName: "Alex",
estimatedDeliveryTime: Date()...Date().addingTimeInterval(1800),
currentStep: .preparing
)
let content = ActivityContent(state: state, staleDate: nil, relevanceScore: 75)
let activity = try Activity.request(attributes: attributes, content: content, pushType: .token)
// Update (optionally with alert)
let updated = ActivityContent(state: newState, staleDate: nil, relevanceScore: 90)
await activity.update(updated)
await activity.update(updated, alertConfiguration: AlertConfiguration(
title: "Order Update", body: "Your driver is nearby!", sound: .default
))
// End
let final = ActivityContent(state: finalState, staleDate: nil, relevanceScore: 0)
await activity.end(final, dismissalPolicy: .after(.now.addingTimeInterval(3600)))
// Button control
struct OpenCameraControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: "OpenCamera") {
ControlWidgetButton(action: OpenCameraIntent()) {
Label("Camera", systemImage: "camera.fill")
}
}
.displayName("Open Camera")
}
}
// Toggle control with value provider
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")
}
}
Use accessory families and 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])
}
}
.systemSmall widgets automatically appear in StandBy (iPhone on charger in landscape). Use @Environment(\.widgetLocation) for conditional rendering:
@Environment(\.widgetLocation) var location
// location == .standBy, .homeScreen, .lockScreen, .carPlay, etc.
Adapt widgets to the Liquid Glass visual style using WidgetAccentedRenderingMode.
| Mode | Description |
|---|---|
.accented | Accented rendering for Liquid Glass |
.accentedDesaturated | Accented with desaturation |
.desaturated | Fully desaturated |
.fullColor | Full-color rendering |
Enable push-based timeline reloads without scheduled polling.
struct MyWidgetPushHandler: WidgetPushHandler {
func pushTokenDidChange(_ pushInfo: WidgetPushInfo, widgets: [WidgetInfo]) {
let tokenString = pushInfo.token.map { String(format: "%02x", $0) }.joined()
// Send tokenString to your server
}
}
.systemSmall widgets render in CarPlay on iOS 26+. Ensure small widget layouts are legible at a glance for driver safety.
Using IntentTimelineProvider instead of AppIntentTimelineProvider. IntentTimelineProvider is deprecated. Use AppIntentTimelineProvider with the App Intents framework.
Exceeding the refresh budget. Widgets have a daily refresh limit. Do not call WidgetCenter.shared.reloadTimelines(ofKind:) on every minor data change. Batch updates and use appropriate TimelineReloadPolicy values.
Forgetting App Groups for shared data. The widget extension runs in a separate process. Use UserDefaults(suiteName:) or a shared App Group container for data the widget reads.
Performing network calls in placeholder(). placeholder(in:) must return synchronously with sample data. Use getTimeline or for async work.
@main is on the WidgetBundle, not on individual widgetsplaceholder(in:) returns synchronously; getSnapshot/snapshot(for:in:) fast when isPreviewreloadTimelines(ofKind:) only on data changeWidgetFamily; accessory widgets tested in .vibrant modeAppIntent with / onlyreferences/widgetkit-advanced.mdWeekly Installs
391
Repository
GitHub Stars
269
First Seen
Mar 3, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex386
kimi-cli383
amp383
cline383
github-copilot383
opencode383
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
105,000 周安装
timeline(for:in:)Missing NSSupportsLiveActivities Info.plist key. Live Activities will not start without NSSupportsLiveActivities = YES in the host app's Info.plist.
Using the deprecated contentState API. Use ActivityContent for all Activity.request, update, and end calls. The contentState-based methods are deprecated.
Not handling the stale state. Check context.isStale in Live Activity views and show a fallback (e.g., "Updating...") when content is outdated.
Putting heavy logic in the widget view. Widget views are rendered in a size-limited process. Pre-compute data in the timeline provider and pass display-ready values through the entry.
Ignoring accessory rendering modes. Lock Screen widgets render in .vibrant or .accented mode, not .fullColor. Test with @Environment(\.widgetRenderingMode) and avoid relying on color alone.
Not testing on device. Dynamic Island and StandBy behavior differ significantly from Simulator. Always verify on physical hardware.
ButtonToggleNSSupportsLiveActivities = YES; ActivityContent used; Dynamic Island closures implementedactivity.end(_:dismissalPolicy:) called; controls use StaticControlConfiguration/AppIntentControlConfiguration