widgetkit

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

WidgetKit 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
references/widgetkit-advanced.md
for timeline strategies, push-based updates, Xcode setup, and advanced patterns.
为iOS 26及以上版本构建主屏幕小组件、锁屏小组件、Live Activities、Dynamic Island(灵动岛)展示、控制中心控件和StandBy(待机)界面。
请查阅
references/widgetkit-advanced.md
了解时间线策略、推送更新、Xcode配置和高级模式。

Workflow

工作流程

1. Create a new widget

1. 创建新的小组件

  1. Add a Widget Extension target in Xcode (File > New > Target > Widget Extension).
  2. Enable App Groups for shared data between the app and widget extension.
  3. Define a
    TimelineEntry
    struct with a
    date
    property and display data.
  4. Implement a
    TimelineProvider
    (static) or
    AppIntentTimelineProvider
    (configurable).
  5. Build the widget view using SwiftUI, adapting layout per
    WidgetFamily
    .
  6. Declare the
    Widget
    conforming struct with a configuration and supported families.
  7. Register all widgets in a
    WidgetBundle
    annotated with
    @main
    .
  1. 在Xcode中添加Widget Extension目标(文件 > 新建 > 目标 > Widget Extension)。
  2. 开启App Groups功能,实现应用与小组件扩展之间的数据共享。
  3. 定义包含
    date
    属性和展示数据的
    TimelineEntry
    结构体。
  4. 实现
    TimelineProvider
    (静态)或
    AppIntentTimelineProvider
    (可配置)。
  5. 使用SwiftUI构建小组件视图,根据
    WidgetFamily
    适配布局。
  6. 声明符合
    Widget
    协议的结构体,包含配置和支持的尺寸系列。
  7. 在标注了
    @main
    WidgetBundle
    中注册所有小组件。

2. Add a Live Activity

2. 添加Live Activity

  1. Define an
    ActivityAttributes
    struct with a nested
    ContentState
    .
  2. Add
    NSSupportsLiveActivities = YES
    to the app's Info.plist.
  3. Create an
    ActivityConfiguration
    in the widget bundle with Lock Screen content and Dynamic Island closures.
  4. Start the activity with
    Activity.request(attributes:content:pushType:)
    .
  5. Update with
    activity.update(_:)
    and end with
    activity.end(_:dismissalPolicy:)
    .
  1. 定义包含嵌套
    ContentState
    ActivityAttributes
    结构体。
  2. 在应用的Info.plist中添加
    NSSupportsLiveActivities = YES
  3. 在小组件bundle中创建
    ActivityConfiguration
    ,包含锁屏内容和Dynamic Island闭包。
  4. 通过
    Activity.request(attributes:content:pushType:)
    启动活动。
  5. 使用
    activity.update(_:)
    更新活动,使用
    activity.end(_:dismissalPolicy:)
    结束活动。

3. Add a Control Center control

3. 添加控制中心控件

  1. Define an
    AppIntent
    for the action.
  2. Create a
    ControlWidgetButton
    or
    ControlWidgetToggle
    in the widget bundle.
  3. Use
    StaticControlConfiguration
    or
    AppIntentControlConfiguration
    .
  1. 为操作定义
    AppIntent
  2. 在小组件bundle中创建
    ControlWidgetButton
    ControlWidgetToggle
  3. 使用
    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
Widget
protocol and returns a
WidgetConfiguration
from its
body
.
swift
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])
    }
}
每个小组件都遵循
Widget
协议,并从其
body
返回
WidgetConfiguration
swift
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
WidgetBundle
to expose multiple widgets from a single extension.
swift
@main
struct MyAppWidgets: WidgetBundle {
    var body: some Widget {
        OrderStatusWidget()
        FavoritesWidget()
        DeliveryActivityWidget()   // Live Activity
        QuickActionControl()       // Control Center
    }
}
使用
WidgetBundle
在单个扩展中暴露多个小组件。
swift
@main
struct MyAppWidgets: WidgetBundle {
    var body: some Widget {
        OrderStatusWidget()
        FavoritesWidget()
        DeliveryActivityWidget()   // Live Activity
        QuickActionControl()       // Control Center
    }
}

Configuration Types

配置类型

Use
StaticConfiguration
for non-configurable widgets. Use
AppIntentConfiguration
(recommended) for configurable widgets paired with
AppIntentTimelineProvider
.
swift
// 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)
}
不可配置的小组件使用
StaticConfiguration
。可配置的小组件推荐使用
AppIntentConfiguration
,搭配
AppIntentTimelineProvider
使用。
swift
// 静态
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

共享修饰符

ModifierPurpose
.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
)
修饰符用途
.configurationDisplayName(_:)
小组件库中显示的名称
.description(_:)
小组件库中显示的描述
.supportedFamilies(_:)
WidgetFamily
值的数组
.supplementalActivityFamilies(_:)
Live Activity尺寸(
.small
,
.medium

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)

系统尺寸(主屏幕)

FamilyPlatform
.systemSmall
iOS, iPadOS, macOS, CarPlay (iOS 26+)
.systemMedium
iOS, iPadOS, macOS
.systemLarge
iOS, iPadOS, macOS
.systemExtraLarge
iPadOS only
尺寸支持平台
.systemSmall
iOS, iPadOS, macOS, CarPlay (iOS 26+)
.systemMedium
iOS, iPadOS, macOS
.systemLarge
iOS, iPadOS, macOS
.systemExtraLarge
仅支持iPadOS

Accessory Families (Lock Screen / watchOS)

附属尺寸(锁屏 / watchOS)

FamilyPlatform
.accessoryCircular
iOS, watchOS
.accessoryRectangular
iOS, watchOS
.accessoryInline
iOS, watchOS
.accessoryCorner
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)
    }
}
尺寸支持平台
.accessoryCircular
iOS, watchOS
.accessoryRectangular
iOS, watchOS
.accessoryInline
iOS, watchOS
.accessoryCorner
仅支持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
Button
and
Toggle
with
AppIntent
conforming types to perform actions directly from a widget without launching the app.
swift
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()
    }
}
Button
Toggle
与遵循
AppIntent
协议的类型搭配使用,无需启动应用即可直接在小组件中执行操作。
swift
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

灵动岛区域

RegionPosition
.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
区域位置
.leading
原深感摄像头左侧,可向下换行
.trailing
原深感摄像头右侧,可向下换行
.center
摄像头正下方
.bottom
所有其他区域下方

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
AccessoryWidgetBackground
.
swift
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])
    }
}
使用附属尺寸和
AccessoryWidgetBackground
swift
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
widgets automatically appear in StandBy (iPhone on charger in landscape). Use
@Environment(\.widgetLocation)
for conditional rendering:
swift
@Environment(\.widgetLocation) var location
// location == .standBy, .homeScreen, .lockScreen, .carPlay, etc.
.systemSmall
尺寸的小组件会自动在StandBy模式中显示(iPhone充电时处于横屏状态)。使用
@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
.
ModeDescription
.accented
Accented rendering for Liquid Glass
.accentedDesaturated
Accented with desaturation
.desaturated
Fully desaturated
.fullColor
Full-color rendering
使用
WidgetAccentedRenderingMode
让小组件适配液态玻璃视觉风格。
模式描述
.accented
液态玻璃强调渲染
.accentedDesaturated
强调加去饱和效果
.desaturated
完全去饱和
.fullColor
全彩渲染

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
widgets render in CarPlay on iOS 26+. Ensure small widget layouts are legible at a glance for driver safety.
.systemSmall
尺寸的小组件可在iOS 26+的CarPlay中渲染。确保小部件布局一目了然,保障驾驶安全。

Common Mistakes

常见错误

  1. Using IntentTimelineProvider instead of AppIntentTimelineProvider.
    IntentTimelineProvider
    is deprecated. Use
    AppIntentTimelineProvider
    with the App Intents framework.
  2. 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.
  3. 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.
  4. Performing network calls in placeholder().
    placeholder(in:)
    must return synchronously with sample data. Use
    getTimeline
    or
    timeline(for:in:)
    for async work.
  5. Missing NSSupportsLiveActivities Info.plist key. Live Activities will not start without
    NSSupportsLiveActivities = YES
    in the host app's Info.plist.
  6. Using the deprecated contentState API. Use
    ActivityContent
    for all
    Activity.request
    ,
    update
    , and
    end
    calls. The
    contentState
    -based methods are deprecated.
  7. Not handling the stale state. Check
    context.isStale
    in Live Activity views and show a fallback (e.g., "Updating...") when content is outdated.
  8. 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.
  9. 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.
  10. Not testing on device. Dynamic Island and StandBy behavior differ significantly from Simulator. Always verify on physical hardware.
  1. 使用IntentTimelineProvider而非AppIntentTimelineProvider
    IntentTimelineProvider
    已废弃,请搭配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. 缺少NSSupportsLiveActivities的Info.plist键。如果主应用的Info.plist中没有配置
    NSSupportsLiveActivities = YES
    ,Live Activities将无法启动。
  6. 使用已废弃的contentState API。所有
    Activity.request
    update
    end
    调用都使用
    ActivityContent
    ,基于
    contentState
    的方法已废弃。
  7. 未处理 stale 状态。在Live Activity视图中检查
    context.isStale
    ,内容过期时显示降级方案(例如"正在更新...")。
  8. 在小组件视图中放置 heavy 逻辑。小组件视图在大小受限的进程中渲染,请在时间线提供器中预先计算数据,通过entry传递可直接展示的值。
  9. 忽略附属渲染模式。锁屏小组件以
    .vibrant
    .accented
    模式渲染,而非
    .fullColor
    。使用
    @Environment(\.widgetRenderingMode)
    进行测试,避免仅依赖颜色传递信息。
  10. 未在真机上测试。灵动岛和StandBy的行为与模拟器差异很大,请务必在物理硬件上验证效果。

Review Checklist

审查清单

  • Widget extension target has App Groups entitlement matching the main app
  • @main
    is on the
    WidgetBundle
    , not on individual widgets
  • placeholder(in:)
    returns synchronously with sample data
  • getSnapshot
    /
    snapshot(for:in:)
    returns quickly when
    context.isPreview
    is true
  • Timeline reload policy matches data update frequency (
    .atEnd
    ,
    .after
    ,
    .never
    )
  • WidgetCenter.shared.reloadTimelines(ofKind:)
    called only when data actually changes
  • Layout adapts per
    WidgetFamily
    using
    @Environment(\.widgetFamily)
  • Accessory widgets use
    AccessoryWidgetBackground
    and test in
    .vibrant
    mode
  • Interactive widgets use
    AppIntent
    with
    Button
    or
    Toggle
    only
  • Live Activity has
    NSSupportsLiveActivities = YES
    in Info.plist
  • ActivityContent
    used (not deprecated
    contentState
    API)
  • Dynamic Island provides all four closures (expanded, compactLeading, compactTrailing, minimal)
  • activity.end(_:dismissalPolicy:)
    called to clean up Live Activities
  • Control widgets use
    StaticControlConfiguration
    or
    AppIntentControlConfiguration
  • 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:)
    同步返回示例数据
  • context.isPreview
    为true时,
    getSnapshot
    /
    snapshot(for:in:)
    快速返回
  • 时间线重载策略与数据更新频率匹配(
    .atEnd
    ,
    .after
    ,
    .never
  • 仅当数据实际变更时才调用
    WidgetCenter.shared.reloadTimelines(ofKind:)
  • 使用
    @Environment(\.widgetFamily)
    根据
    WidgetFamily
    适配布局
  • 附属小组件使用
    AccessoryWidgetBackground
    ,并在
    .vibrant
    模式下测试
  • 交互式小组件仅搭配
    AppIntent
    使用
    Button
    Toggle
  • Live Activity在Info.plist中配置了
    NSSupportsLiveActivities = YES
  • 使用了
    ActivityContent
    (而非废弃的
    contentState
    API)
  • 灵动岛提供了全部四个闭包(expanded, compactLeading, compactTrailing, minimal)
  • 调用了
    activity.end(_:dismissalPolicy:)
    清理Live Activities
  • 控制中心小组件使用了
    StaticControlConfiguration
    AppIntentControlConfiguration
  • 小组件在真机上测试了StandBy、灵动岛和锁屏效果
  • iOS 26液态玻璃渲染已通过
    WidgetAccentedRenderingMode
    测试
  • 确保Timeline entry和Intent类型符合Sendable要求;访问共享状态时,小组件配置提供器应标注@MainActor隔离

References

参考资料