widgetkit

widgetkit

熱門

實作、審查或改善 WidgetKit 的小工具與控制項目。用於建置主畫面、鎖定畫面、待機模式或 CarPlay 的小工具,搭配時間軸提供者;可設定的小工具搭配 AppIntentTimelineProvider;互動式小工具或控制中心的按鈕/切換開關;WidgetKit 推播重新整理、更新配額、深層連結、智慧堆疊關聯性、Liquid Glass/強調色渲染、小工具擴充設定、WidgetBundle、App Groups 與權利。

932星標
47分支
更新於 2026/7/15
SKILL.md
唯讀
名稱
widgetkit
描述

實作、審查或改善 WidgetKit 的小工具與控制項目。用於建置主畫面、鎖定畫面、待機模式或 CarPlay 的小工具,搭配時間軸提供者;可設定的小工具搭配 AppIntentTimelineProvider;互動式小工具或控制中心的按鈕/切換開關;WidgetKit 推播重新整理、更新配額、深層連結、智慧堆疊關聯性、Liquid Glass/強調色渲染、小工具擴充設定、WidgetBundle、App Groups 與權利。

WidgetKit

建置 iOS 26+ 的主畫面小工具、鎖定畫面小工具、控制中心控制項,以及待機模式或 CarPlay 小工具介面。

將相鄰框架的指引範圍限定在 WidgetKit 整合。僅在 ActivityKit 和 App Intents 直接連接到 WidgetKit 介面時才納入;將完整的生命週期、APNs content-state、Siri/捷徑/Spotlight 或實體建模工作交給同級的 activitykitapp-intents 技能。

請參閱 references/widgetkit-advanced.md 了解時間軸策略、推播更新、Xcode 設定及進階模式。

目錄

工作流程

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. 在標註 @mainWidgetBundle 中註冊所有小工具。

2. 整合相鄰介面

  1. 當 App 有即時活動時,在小工具 bundle 中註冊 ActivityConfiguration,但將 ActivityAttributes、請求/更新/結束、APNs content-state 及動態島佈局深度保留在 activitykit 中。
  2. 在 WidgetKit 檢視或控制項中放置 ButtonToggleControlWidgetButtonControlWidgetToggle,但將意圖建模、實體、查詢、Siri、捷徑和 Spotlight 保留在 app-intents 中。

3. 新增控制中心控制項

  1. 為按鈕重複使用 AppIntent/OpenIntent,或為切換開關使用 SetValueIntent
  2. 在小工具 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("訂單狀態")
        .description("追蹤您目前的訂單。")
        .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(_:) 即時活動尺寸(.small.medium

TimelineProvider

用於靜態(不可設定)小工具。使用 completion handler。需要三個方法:

struct WeatherProvider: TimelineProvider {
    typealias Entry = WeatherEntry

    func placeholder(in context: Context) -> WeatherEntry {
        WeatherEntry(date: .now, temperature: 72, condition: "晴朗")
    }

    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: "範例", 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)
    }
}

互動式小工具 (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 在小工具擴充中註冊即時活動介面。將此部分保留為註冊與渲染交接;使用 activitykit 處理 ActivityAttributes、生命週期、推播更新及完整的動態島模式。

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("相機", 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("手電筒")
    }
}

鎖定畫面小工具

使用 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 等

小工具 URL 處理與深層連結

使用一個 .widgetURL(_:) 作為整個小工具的備用路由。僅在系列與佈局支援的情況下,使用 Link 作為明確的子目標,包括 .accessoryRectangular.systemSmall 及更大的系統小工具。對於小型小工具,偏好一個明確的備用路由;除非視覺提示與點擊區域保持明確,否則避免使用多個 Link 目標。

切勿在層級中附加多個 widgetURL 修飾詞。

智慧堆疊關聯性

在時間軸條目上使用 TimelineEntryRelevance(score:duration:) 以實現 iPhone 與 iPad 智慧堆疊的即時關聯性。將分數保持在一致的正數範圍內;零或更低表示不相關。

對於可設定小工具,從 App 端程式碼捐贈對應於使用者動作或小工具參數的 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 類型保留在小工具擴充目標或連結到其中的共享程式碼中,而不僅在主 App 目標中。
  • 使用 .pushHandler(...) 在小工具設定上註冊處理器。
  • 不要使用 User Notifications 註冊來取得小工具推播權杖;WidgetKit 透過 pushTokenDidChange(_:widgets:) 提供權杖。
  • 使用 apns-push-type: widgets、主題後綴 .push-type.widgetsaps.content-changed
  • 將推播視為有預算的、機會主義的重新整理訊號,而非狀態傳遞,也非唯一的新鮮度模型。時間軸、重新載入策略、共享儲存或重新擷取,以及 App 觸發的 WidgetCenter 重新載入,仍然是備用路徑。

控制項推播重新整理:

  • 使用 .pushHandler(...)ControlWidgetConfiguration 上註冊 ControlPushHandler
  • pushTokensDidChange(controls:) 接收 [ControlInfo];從每個控制項的 pushInfo 讀取權杖。
  • 使用 apns-push-type: controls、主題後綴 .push-type.controlsaps.content-changed

CarPlay 小工具

小型系統小工具可在 iOS 26+ 的 CarPlay 中出現。確保佈局一目了然;點擊與控制項取決於車輛觸控支援,以及開啟 App 的 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 相符的 App Groups 權利
  • [ ] @main 位於 WidgetBundle 上,而非個別小工具
  • [ ] placeholder(in:) 同步回傳;getSnapshot/snapshot(for:in:)isPreview 時快速
  • [ ] 時間軸重新載入策略符合更新頻率;僅在資料變更時使用 reloadTimelines(ofKind:)
  • [ ] 佈局根據 WidgetFamily 調整;accessory 小工具已在 .vibrant 模式下測試
  • [ ] 互動式小工具使用擴充可用的 App Intents,僅搭配 Button/Toggle
  • [ ] 使用一個 .widgetURL(_:) 備用路由;Link 子目標符合系列要求
  • [ ] 小工具推播處理器位於小工具擴充/共享程式碼中,且不使用 User Notifications 權杖註冊
  • [ ] 小工具/控制項推播補充時間軸與共享狀態/重新擷取備用方案
  • [ ] 智慧堆疊關聯性使用時間軸關聯性,並在適當時使用 App 端意圖捐贈
  • [ ] 即時活動生命週期與 App Intent 建模已移交給同級技能
  • [ ] 控制項使用 StaticControlConfiguration/AppIntentControlConfiguration
  • [ ] 時間軸條目與 Intent 類型為 Sendable;已在裝置上測試

參考資料