widgetkit
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWidgetKit and ActivityKit
WidgetKit与ActivityKit
Build home screen widgets, Lock Screen widgets, Live Activities, Dynamic Island
presentations, Control Center controls, and StandBy surfaces for iOS 26+.
See for timeline strategies, push-based
updates, Xcode setup, and advanced patterns.
references/widgetkit-advanced.md为iOS 26及以上版本构建主屏幕小组件、锁屏小组件、Live Activities、Dynamic Island(灵动岛)展示、控制中心控件和StandBy(待机)界面。
请查阅了解时间线策略、推送更新、Xcode配置和高级模式。
references/widgetkit-advanced.mdWorkflow
工作流程
1. Create a new widget
1. 创建新的小组件
- Add a Widget Extension target in Xcode (File > New > Target > Widget Extension).
- Enable App Groups for shared data between the app and widget extension.
- Define a struct with a
TimelineEntryproperty and display data.date - Implement a (static) or
TimelineProvider(configurable).AppIntentTimelineProvider - Build the widget view using SwiftUI, adapting layout per .
WidgetFamily - Declare the conforming struct with a configuration and supported families.
Widget - Register all widgets in a annotated with
WidgetBundle.@main
- 在Xcode中添加Widget Extension目标(文件 > 新建 > 目标 > Widget Extension)。
- 开启App Groups功能,实现应用与小组件扩展之间的数据共享。
- 定义包含属性和展示数据的
date结构体。TimelineEntry - 实现(静态)或
TimelineProvider(可配置)。AppIntentTimelineProvider - 使用SwiftUI构建小组件视图,根据适配布局。
WidgetFamily - 声明符合协议的结构体,包含配置和支持的尺寸系列。
Widget - 在标注了的
@main中注册所有小组件。WidgetBundle
2. Add a Live Activity
2. 添加Live Activity
- Define an struct with a nested
ActivityAttributes.ContentState - Add to the app's Info.plist.
NSSupportsLiveActivities = YES - Create an in the widget bundle with Lock Screen content and Dynamic Island closures.
ActivityConfiguration - Start the activity with .
Activity.request(attributes:content:pushType:) - Update with and end with
activity.update(_:).activity.end(_:dismissalPolicy:)
- 定义包含嵌套的
ContentState结构体。ActivityAttributes - 在应用的Info.plist中添加。
NSSupportsLiveActivities = YES - 在小组件bundle中创建,包含锁屏内容和Dynamic Island闭包。
ActivityConfiguration - 通过启动活动。
Activity.request(attributes:content:pushType:) - 使用更新活动,使用
activity.update(_:)结束活动。activity.end(_:dismissalPolicy:)
3. Add a Control Center control
3. 添加控制中心控件
- Define an for the action.
AppIntent - Create a or
ControlWidgetButtonin the widget bundle.ControlWidgetToggle - Use or
StaticControlConfiguration.AppIntentControlConfiguration
- 为操作定义。
AppIntent - 在小组件bundle中创建或
ControlWidgetButton。ControlWidgetToggle - 使用或
StaticControlConfiguration。AppIntentControlConfiguration
4. Review existing widget code
4. 审查现有小组件代码
Run through the Review Checklist at the end of this document.
对照本文档末尾的审查清单逐一检查。
Widget Protocol and WidgetBundle
Widget协议与WidgetBundle
Widget
Widget
Every widget conforms to the protocol and returns a
from its .
WidgetWidgetConfigurationbodyswift
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])
}
}每个小组件都遵循协议,并从其返回。
WidgetbodyWidgetConfigurationswift
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
Use to expose multiple widgets from a single extension.
WidgetBundleswift
@main
struct MyAppWidgets: WidgetBundle {
var body: some Widget {
OrderStatusWidget()
FavoritesWidget()
DeliveryActivityWidget() // Live Activity
QuickActionControl() // Control Center
}
}使用在单个扩展中暴露多个小组件。
WidgetBundleswift
@main
struct MyAppWidgets: WidgetBundle {
var body: some Widget {
OrderStatusWidget()
FavoritesWidget()
DeliveryActivityWidget() // Live Activity
QuickActionControl() // Control Center
}
}Configuration Types
配置类型
Use for non-configurable widgets. Use
(recommended) for configurable widgets paired with .
StaticConfigurationAppIntentConfigurationAppIntentTimelineProviderswift
// 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)
}不可配置的小组件使用。可配置的小组件推荐使用,搭配使用。
StaticConfigurationAppIntentConfigurationAppIntentTimelineProviderswift
// 静态
StaticConfiguration(kind: "MyWidget", provider: MyProvider()) { entry in
MyWidgetView(entry: entry)
}
// 可配置
AppIntentConfiguration(kind: "ConfigWidget", intent: SelectCategoryIntent.self,
provider: CategoryProvider()) { entry in
CategoryWidgetView(entry: entry)
}Shared Modifiers
共享修饰符
| Modifier | Purpose |
|---|---|
| Name shown in the widget gallery |
| Description shown in the widget gallery |
| Array of |
| Live Activity sizes ( |
| 修饰符 | 用途 |
|---|---|
| 小组件库中显示的名称 |
| 小组件库中显示的描述 |
| |
| Live Activity尺寸( |
TimelineProvider
TimelineProvider
For static (non-configurable) widgets. Uses completion handlers. Three required methods:
swift
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)))
}
}
}适用于静态(不可配置)小组件,使用完成回调,包含三个必填方法:
swift
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
AppIntentTimelineProvider
For configurable widgets. Uses async/await natively. Receives user intent configuration.
swift
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)
}
}适用于可配置小组件,原生支持async/await,可接收用户意图配置。
swift
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 Families
小组件尺寸系列
System Families (Home Screen)
系统尺寸(主屏幕)
| Family | Platform |
|---|---|
| iOS, iPadOS, macOS, CarPlay (iOS 26+) |
| iOS, iPadOS, macOS |
| iOS, iPadOS, macOS |
| iPadOS only |
| 尺寸 | 支持平台 |
|---|---|
| iOS, iPadOS, macOS, CarPlay (iOS 26+) |
| iOS, iPadOS, macOS |
| iOS, iPadOS, macOS |
| 仅支持iPadOS |
Accessory Families (Lock Screen / watchOS)
附属尺寸(锁屏 / watchOS)
| Family | Platform |
|---|---|
| iOS, watchOS |
| iOS, watchOS |
| iOS, watchOS |
| watchOS only |
Adapt layout per family using :
@Environment(\.widgetFamily)swift
@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, watchOS |
| iOS, watchOS |
| iOS, watchOS |
| 仅支持watchOS |
使用根据尺寸适配布局:
@Environment(\.widgetFamily)swift
@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)
}
}Interactive Widgets (iOS 17+)
交互式小组件(iOS 17+)
Use and with conforming types to perform actions
directly from a widget without launching the app.
ButtonToggleAppIntentswift
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()
}
}将和与遵循协议的类型搭配使用,无需启动应用即可直接在小组件中执行操作。
ButtonToggleAppIntentswift
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()
}
}Live Activities and Dynamic Island
Live Activities与灵动岛
ActivityAttributes
ActivityAttributes
Define the static and dynamic data model.
swift
struct DeliveryAttributes: ActivityAttributes {
struct ContentState: Codable, Hashable {
var driverName: String
var estimatedDeliveryTime: ClosedRange<Date>
var currentStep: DeliveryStep
}
var orderNumber: Int
var restaurantName: String
}定义静态和动态数据模型。
swift
struct DeliveryAttributes: ActivityAttributes {
struct ContentState: Codable, Hashable {
var driverName: String
var estimatedDeliveryTime: ClosedRange<Date>
var currentStep: DeliveryStep
}
var orderNumber: Int
var restaurantName: String
}ActivityConfiguration
ActivityConfiguration
Provide Lock Screen content and Dynamic Island closures in the widget bundle.
swift
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")
}
}
}
}在小组件bundle中提供锁屏内容和灵动岛闭包。
swift
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")
}
}
}
}Dynamic Island Regions
灵动岛区域
| Region | Position |
|---|---|
| Left of the TrueDepth camera; wraps below |
| Right of the TrueDepth camera; wraps below |
| Directly below the camera |
| Below all other regions |
| 区域 | 位置 |
|---|---|
| 原深感摄像头左侧,可向下换行 |
| 原深感摄像头右侧,可向下换行 |
| 摄像头正下方 |
| 所有其他区域下方 |
Starting, Updating, and Ending
启动、更新和结束
swift
// 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)))swift
// 启动
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: "Order Update", body: "Your driver is nearby!", sound: .default
))
// 结束
let final = ActivityContent(state: finalState, staleDate: nil, relevanceScore: 0)
await activity.end(final, dismissalPolicy: .after(.now.addingTimeInterval(3600)))Control Center Widgets (iOS 18+)
控制中心小组件(iOS 18+)
swift
// 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")
}
}swift
// 按钮控件
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")
}
}Lock Screen Widgets
锁屏小组件
Use accessory families and .
AccessoryWidgetBackgroundswift
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])
}
}使用附属尺寸和。
AccessoryWidgetBackgroundswift
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])
}
}StandBy Mode
StandBy模式
.systemSmall@Environment(\.widgetLocation)swift
@Environment(\.widgetLocation) var location
// location == .standBy, .homeScreen, .lockScreen, .carPlay, etc..systemSmall@Environment(\.widgetLocation)swift
@Environment(\.widgetLocation) var location
// location == .standBy, .homeScreen, .lockScreen, .carPlay, etc.iOS 26 Additions
iOS 26新增功能
Liquid Glass Support
液态玻璃支持
Adapt widgets to the Liquid Glass visual style using .
WidgetAccentedRenderingMode| Mode | Description |
|---|---|
| Accented rendering for Liquid Glass |
| Accented with desaturation |
| Fully desaturated |
| Full-color rendering |
使用让小组件适配液态玻璃视觉风格。
WidgetAccentedRenderingMode| 模式 | 描述 |
|---|---|
| 液态玻璃强调渲染 |
| 强调加去饱和效果 |
| 完全去饱和 |
| 全彩渲染 |
WidgetPushHandler
WidgetPushHandler
Enable push-based timeline reloads without scheduled polling.
swift
struct MyWidgetPushHandler: WidgetPushHandler {
func pushTokenDidChange(_ pushInfo: WidgetPushInfo, widgets: [WidgetInfo]) {
let tokenString = pushInfo.token.map { String(format: "%02x", $0) }.joined()
// Send tokenString to your server
}
}无需定时轮询,启用推送驱动的时间线重载。
swift
struct MyWidgetPushHandler: WidgetPushHandler {
func pushTokenDidChange(_ pushInfo: WidgetPushInfo, widgets: [WidgetInfo]) {
let tokenString = pushInfo.token.map { String(format: "%02x", $0) }.joined()
// 发送tokenString到你的服务器
}
}CarPlay Widgets
CarPlay小组件
.systemSmall.systemSmallCommon Mistakes
常见错误
-
Using IntentTimelineProvider instead of AppIntentTimelineProvider.is deprecated. Use
IntentTimelineProviderwith the App Intents framework.AppIntentTimelineProvider -
Exceeding the refresh budget. Widgets have a daily refresh limit. Do not callon every minor data change. Batch updates and use appropriate
WidgetCenter.shared.reloadTimelines(ofKind:)values.TimelineReloadPolicy -
Forgetting App Groups for shared data. The widget extension runs in a separate process. Useor a shared App Group container for data the widget reads.
UserDefaults(suiteName:) -
Performing network calls in placeholder().must return synchronously with sample data. Use
placeholder(in:)orgetTimelinefor async work.timeline(for:in:) -
Missing NSSupportsLiveActivities Info.plist key. Live Activities will not start withoutin the host app's Info.plist.
NSSupportsLiveActivities = YES -
Using the deprecated contentState API. Usefor all
ActivityContent,Activity.request, andupdatecalls. Theend-based methods are deprecated.contentState -
Not handling the stale state. Checkin Live Activity views and show a fallback (e.g., "Updating...") when content is outdated.
context.isStale -
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 inor
.vibrantmode, not.accented. Test with.fullColorand avoid relying on color alone.@Environment(\.widgetRenderingMode) -
Not testing on device. Dynamic Island and StandBy behavior differ significantly from Simulator. Always verify on physical hardware.
- 使用IntentTimelineProvider而非AppIntentTimelineProvider。已废弃,请搭配App Intents框架使用
IntentTimelineProvider。AppIntentTimelineProvider - 超出刷新预算。小组件有每日刷新上限,不要在每次数据小幅变动时都调用。请批量处理更新,使用合适的
WidgetCenter.shared.reloadTimelines(ofKind:)值。TimelineReloadPolicy - 忘记为共享数据配置App Groups。小组件扩展运行在独立进程中,使用或共享App Group容器存储小组件需要读取的数据。
UserDefaults(suiteName:) - 在placeholder()中执行网络请求。必须同步返回示例数据,异步工作请放在
placeholder(in:)或getTimeline中执行。timeline(for:in:) - 缺少NSSupportsLiveActivities的Info.plist键。如果主应用的Info.plist中没有配置,Live Activities将无法启动。
NSSupportsLiveActivities = YES - 使用已废弃的contentState API。所有、
Activity.request和update调用都使用end,基于ActivityContent的方法已废弃。contentState - 未处理 stale 状态。在Live Activity视图中检查,内容过期时显示降级方案(例如"正在更新...")。
context.isStale - 在小组件视图中放置 heavy 逻辑。小组件视图在大小受限的进程中渲染,请在时间线提供器中预先计算数据,通过entry传递可直接展示的值。
- 忽略附属渲染模式。锁屏小组件以或
.vibrant模式渲染,而非.accented。使用.fullColor进行测试,避免仅依赖颜色传递信息。@Environment(\.widgetRenderingMode) - 未在真机上测试。灵动岛和StandBy的行为与模拟器差异很大,请务必在物理硬件上验证效果。
Review Checklist
审查清单
- Widget extension target has App Groups entitlement matching the main app
- is on the
@main, not on individual widgetsWidgetBundle - returns synchronously with sample data
placeholder(in:) - /
getSnapshotreturns quickly whensnapshot(for:in:)is truecontext.isPreview - Timeline reload policy matches data update frequency (,
.atEnd,.after).never - called only when data actually changes
WidgetCenter.shared.reloadTimelines(ofKind:) - Layout adapts per using
WidgetFamily@Environment(\.widgetFamily) - Accessory widgets use and test in
AccessoryWidgetBackgroundmode.vibrant - Interactive widgets use with
AppIntentorButtononlyToggle - Live Activity has in Info.plist
NSSupportsLiveActivities = YES - used (not deprecated
ActivityContentAPI)contentState - Dynamic Island provides all four closures (expanded, compactLeading, compactTrailing, minimal)
- called to clean up Live Activities
activity.end(_:dismissalPolicy:) - Control widgets use or
StaticControlConfigurationAppIntentControlConfiguration - Widget tested on device for StandBy, Dynamic Island, and Lock Screen
- iOS 26 Liquid Glass rendering tested with
WidgetAccentedRenderingMode - Ensure Timeline entries and Intent types are Sendable; widget configuration providers should be @MainActor-isolated if they access shared state
- 小组件扩展目标的App Groups权限与主应用匹配
- 标注在
@main上,而非单个小组件WidgetBundle - 同步返回示例数据
placeholder(in:) - 当为true时,
context.isPreview/getSnapshot快速返回snapshot(for:in:) - 时间线重载策略与数据更新频率匹配(,
.atEnd,.after).never - 仅当数据实际变更时才调用
WidgetCenter.shared.reloadTimelines(ofKind:) - 使用根据
@Environment(\.widgetFamily)适配布局WidgetFamily - 附属小组件使用,并在
AccessoryWidgetBackground模式下测试.vibrant - 交互式小组件仅搭配使用
AppIntent或ButtonToggle - Live Activity在Info.plist中配置了
NSSupportsLiveActivities = YES - 使用了(而非废弃的
ActivityContentAPI)contentState - 灵动岛提供了全部四个闭包(expanded, compactLeading, compactTrailing, minimal)
- 调用了清理Live Activities
activity.end(_:dismissalPolicy:) - 控制中心小组件使用了或
StaticControlConfigurationAppIntentControlConfiguration - 小组件在真机上测试了StandBy、灵动岛和锁屏效果
- iOS 26液态玻璃渲染已通过测试
WidgetAccentedRenderingMode - 确保Timeline entry和Intent类型符合Sendable要求;访问共享状态时,小组件配置提供器应标注@MainActor隔离
References
参考资料
- Advanced guide:
references/widgetkit-advanced.md - Apple docs: WidgetKit | ActivityKit | Keeping a widget up to date
- 高级指南:
references/widgetkit-advanced.md - Apple官方文档:WidgetKit | ActivityKit | 保持小组件内容最新