mcpField
A
axiom-extensions-widgets-ref

axiom-extensions-widgets-ref

github.com/charleswiltgen開発1.2k185 回インストール今日更新
リポジトリ

axiom-extensions-widgets-ref について

Apple のウィジェットや App Extensions を Swift で開発するための API リファレンスです。設定、タイムライン、インタラクティブウィジェット、Live Activities を扱います。

主な機能

  1. 01標準および設定可能なウィジェット
  2. 02App Intents による操作対応
  3. 03タイムラインと更新ポリシー
  4. 04Live Activities と Dynamic Island
  5. 05拡張機能のデータ共有とライフサイクル

活用シーン

  1. 01ホーム画面やロック画面のウィジェット実装
  2. 02Live Activities の作成
  3. 03アプリと拡張機能間のデータ共有

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-extensions-widgets-ref

インストール後、Claude Code で/axiom-extensions-widgets-refを入力して読み込まれたか確認します。

詳細

拡張機能とウィジェットの API リファレンス

概要

このスキルでは、Apple のウィジェットと拡張機能のエコシステムについて、包括的な API リファレンスを提供します。

  • 標準ウィジェット(iOS 14 以降)— ホーム画面、ロック画面、スタンバイのウィジェット
  • インタラクティブウィジェット(iOS 17 以降)— App Intents を使用したボタンと切り替え
  • ライブアクティビティ(iOS 16.1 以降)— ロック画面とダイナミックアイランドでのリアルタイム更新
  • コントロールセンターウィジェット(iOS 18 以降)— システム全体で使えるクイックコントロール
  • Liquid Glass ウィジェット(iOS 26 以降)— アクセント付きのレンダリング、ガラス効果、コンテナ背景
  • visionOS ウィジェット(visionOS 2 以降)— マウントスタイル、テクスチャ、近接認識
  • アプリ拡張機能 — 共有データ、ライフサイクル、エンタイトルメント

ウィジェットは、システムがタイムライン上にレンダリングする SwiftUI のアーカイブ済みスナップショットです。拡張機能は、アプリにバンドルされるサンドボックス化された実行ファイルです。

このスキルを使用する場面

✅ 次の場合にこのスキルを使用してください:

  • あらゆる種類のウィジェット(ホーム画面、ロック画面、スタンバイ)を実装する
  • 継続中のイベントのライブアクティビティを作成する
  • コントロールセンターのコントロールを構築する
  • アプリと拡張機能の間でデータを共有する
  • ウィジェットのタイムラインと更新ポリシーを理解する
  • ウィジェットを App Intents と連携する
  • ウィジェットで Liquid Glass のレンダリングを採用する
  • watchOS または visionOS のウィジェットに対応する
  • visionOS のマウントスタイル、テクスチャ、近接認識を実装する

❌ 次の場合はこのスキルを使用しないでください:

  • App Intents のみについての質問(app-intents-ref スキルを使用)
  • SwiftUI のレイアウトに関する問題(swiftui-layout スキルを使用)
  • パフォーマンスの最適化(swiftui-performance スキルを使用)
  • クラッシュのデバッグ(xcode-debugging スキルを使用)

関連スキル

  • app-intents-ref — インタラクティブウィジェットと設定のための App Intents
  • swift-concurrency — ウィジェットのデータ読み込みにおける非同期/待機パターン
  • swiftui-performance — ウィジェットのレンダリングの最適化
  • swiftui-layout — 複雑なウィジェットのレイアウト
  • extensions-widgets — アンチパターンとデバッグを扱う分野別スキル

主な用語

  • タイムライン — 表示するコンテンツとそのタイミングを定義するエントリーの一覧。システムは指定された時刻にエントリーを表示します
  • TimelineProvider — タイムラインエントリーを提供するプロトコル(プレースホルダー、スナップショット、タイムラインの生成)
  • TimelineEntry — ウィジェットのデータと表示日時を含む構造体
  • タイムライン予算 — タイムラインの再読み込みに対する1日の上限(40~70回)
  • 予算対象外 — 回数に含まれない再読み込み(ユーザー起因、アプリのフォアグラウンド化、システム起因)
  • ウィジェットファミリー — サイズ/形状(systemSmall、systemMedium、accessoryCircular など)
  • App Groups — アプリと拡張機能の間で共有するデータコンテナのためのエンタイトルメント
  • ActivityAttributes — 静的データ(一度設定)と動的な ContentState(ライフサイクル中に更新)
  • ContentState — ActivityAttributes の変化する部分。合計で 4KB 未満である必要があります
  • ダイナミックアイランド — iPhone 14 Pro 以降でのライブアクティビティの表示。コンパクト、最小、展開の各サイズがあります
  • ControlWidget — コントロールセンター、ロック画面、アクションボタン用の iOS 18 以降のウィジェット
  • 補助アクティビティファミリー — Apple Watch または CarPlay でライブアクティビティを利用可能にします

パート 1:標準ウィジェット(iOS 14 以降)

ウィジェットの構成タイプ

StaticConfiguration

ユーザー設定を必要としないウィジェットに使用します。

@main
struct MyWidget: Widget {
    let kind: String = "MyWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            MyWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("This widget displays...")
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
    }
}
AppIntentConfiguration(iOS 17 以降)

App Intents を使用してユーザーが設定できるウィジェットに使用します。

struct MyConfigurableWidget: Widget {
    let kind: String = "MyConfigurableWidget"

    var body: some WidgetConfiguration {
        AppIntentConfiguration(
            kind: kind,
            intent: SelectProjectIntent.self,
            provider: Provider()
        ) { entry in
            MyWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("Project Status")
        .description("Shows your selected project")
    }
}

IntentConfiguration からの移行:iOS 16 以前では、SiriKit インテントを使用する IntentConfiguration が使われていました。iOS 17 以降では AppIntentConfiguration に移行してください。

ActivityConfiguration

ライブアクティビティに使用します(「ライブアクティビティ」セクションで説明します)。

適切な構成の選択

ユーザー構成が不要ですか?StaticConfiguration を使用します。シンプルな固定オプションですか?WidgetConfigurationIntent を使用する AppIntentConfiguration を使用します。アプリデータから動的にオプションを取得しますか?AppIntentConfiguration と EntityQuery を組み合わせて使用します。

クイックリファレンス:

  • StaticConfiguration — カスタマイズなし(天気、バッテリー残量)
  • AppIntentConfiguration(シンプル)— 固定オプション(タイマーのプリセット、テーマの選択)
  • AppIntentConfiguration(EntityQuery)— アプリデータから取得する動的なリスト(プロジェクト/連絡先/プレイリストの選択)
  • ActivityConfiguration — 継続中のライブイベント(配達状況の追跡、ワークアウトの進捗、スポーツのスコア)

ウィジェットファミリー

システムファミリー(ホーム画面)
  • systemSmall(約170×170、iOS 14+)— 単一の情報、アイコン
  • systemMedium(約360×170、iOS 14+)— 複数のデータポイント、グラフ
  • systemLarge(約360×380、iOS 14+)— 詳細ビュー、リスト
  • systemExtraLarge(約720×380、iOS 15+、iPadのみ)— 豊富なレイアウト、複数のビュー
アクセサリファミリー(ロック画面、iOS 16+)
  • accessoryCircular(約48×48pt)— 円形のコンプリケーション、アイコンまたはゲージ
  • accessoryRectangular(約160×72pt)— 時計の上、テキスト+アイコン
  • accessoryInline(1行)— 日付の上、テキストのみ
例:複数のファミリーへの対応
struct MyWidget: Widget {
    var body: some WidgetConfiguration {
        StaticConfiguration(kind: "MyWidget", provider: Provider()) { entry in
            if #available(iOSApplicationExtension 16.0, *) {
                switch entry.family {
                case .systemSmall:
                    SmallWidgetView(entry: entry)
                case .systemMedium:
                    MediumWidgetView(entry: entry)
                case .accessoryCircular:
                    CircularWidgetView(entry: entry)
                case .accessoryRectangular:
                    RectangularWidgetView(entry: entry)
                default:
                    Text("Unsupported")
                }
            } else {
                LegacyWidgetView(entry: entry)
            }
        }
        .supportedFamilies([
            .systemSmall,
            .systemMedium,
            .accessoryCircular,
            .accessoryRectangular
        ])
    }
}

タイムラインシステム

TimelineProviderプロトコル

システムがウィジェットをいつ表示するかを定義するエントリーを提供します。

struct Provider: TimelineProvider {
    // Placeholder while loading
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), emoji: "😀")
    }

    // Shown in widget gallery
    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date(), emoji: "📷")
        completion(entry)
    }

    // Actual timeline
    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        var entries: [SimpleEntry] = []
        let currentDate = Date()

        // Create entry every hour for 5 hours
        for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(date: entryDate, emoji: "⏰")
            entries.append(entry)
        }

        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}
TimelineReloadPolicy

システムが新しいタイムラインを要求するタイミングを制御します。

  • .atEnd — 最後のエントリーの後に再読み込みします
  • .after(date) — 指定した日時に再読み込みします
  • .never — 自動再読み込みなし(手動のみ)
手動での再読み込み
import WidgetKit

// Reload all widgets of this kind
WidgetCenter.shared.reloadAllTimelines()

// Reload specific kind
WidgetCenter.shared.reloadTimelines(ofKind: "MyWidget")

パフォーマンスと予算のクイックリファレンス

タイムラインの更新予算
  • 1日の予算:1日あたり40~70回の再読み込み(システム負荷と利用状況によって異なります)
  • 予算の対象外:ユーザーによる再読み込み、アプリをフォアグラウンドにする、ウィジェットの追加、システムの再起動
  • 計画的(1時間に4回)— 1日あたり約48回の再読み込み、バッテリーへの影響は小さめ
  • 高頻度(1時間に12回)— 午後6時までに予算を使い切り、影響は大きめ
  • オンデマンドのみ — 1日あたり5~10回の再読み込み、影響は最小限
  • 重要なデータ変更や時間ベースのイベントに応じて再読み込みします。推測に基づく再読み込みや見た目だけの再読み込みは避けてください。
// ✅ GOOD: Strategic intervals (15-60 min)
let entries = (0..<8).map { offset in
    let date = Calendar.current.date(byAdding: .minute, value: offset * 15, to: now)!
    return SimpleEntry(date: date, data: data)
}
メモリ制限
  • 標準ウィジェットは約30MB、Live Activitiesは約50MBです。超過するとシステムによって終了されます
  • 必要なデータだけを読み込みます(例:データベース全体ではなく loadRecentItems(limit: 10) を使用)
ネットワークリクエスト

ウィジェットのビュー内でネットワークリクエストを実行しないでください — 表示前に完了しません。代わりに getTimeline() でデータを取得してください。

タイムラインの生成

getTimeline() は5秒以内に完了させます。負荷の高い計算はメインアプリでキャッシュし、共有コンテナから事前計算済みのデータを読み込み、エントリー数は10~20件に制限します。

ビューのレンダリング

すべての処理を TimelineEntry で事前計算し、ビューをシンプルに保ちます。body 内で負荷の高い処理を行わないでください。

画像
  • アセットカタログの画像またはSF Symbolsを使用します(高速)
  • 共有コンテナ内の小さな画像は使用できます
  • AsyncImage はウィジェットでは動作しません
  • 大きな画像を使うと、メモリ超過により終了されます

パート2:インタラクティブウィジェット(iOS 17+)

ButtonとToggle

インタラクティブウィジェットでは、App IntentsとともにSwiftUIの Button と Toggle を使用します。

App Intentを使用するButton
Button(intent: IncrementIntent()) {
    Label("Increment", systemImage: "plus.circle")
}

インテントは perform() メソッドでApp Groups経由の共有データを更新します。AppIntent の定義構文全体については axiom-app-intents-ref を参照してください。

App Intentを使用するToggle

Buttonと同じパターンです。状態にバインドした Toggle を使用し、変更時にインテントを呼び出します。

Toggle(isOn: $isEnabled) {
    Text("Feature")
}
.onChange(of: isEnabled) { newValue in
    Task { try? await ToggleFeatureIntent(enabled: newValue).perform() }
}

インテントは同じ AppIntent 構造に従い、@Parameter(title: "Enabled") var enabled: Bool を使用します。AppIntent の定義構文全体については axiom-app-intents-ref を参照してください。

invalidatableContentモディファイア

App Intentの実行中に視覚的なフィードバックを提供します。

struct MyWidgetView: View {
    var entry: Provider.Entry

    var body: some View {
        VStack {
            Text(entry.status)
                .invalidatableContent() // Dims during intent execution

            Button(intent: RefreshIntent()) {
                Image(systemName: "arrow.clockwise")
            }
        }
    }
}

効果:.invalidatableContent() を含むコンテンツは、関連付けられたインテントの実行中、少し透明になり、ユーザーにフィードバックを提供します。

アニメーションシステム

数値テキストのcontentTransition
Text("\(entry.value)")
    .contentTransition(.numericText(value: Double(entry.value)))

効果:数値が瞬時に変わるのではなく、滑らかにカウントアップまたはカウントダウンします。

ビュートランジション
VStack {
    if entry.showDetail {
        DetailView()
            .transition(.scale.combined(with: .opacity))
    }
}
.animation(.spring(response: 0.3), value: entry.showDetail)

パート 3:設定可能なウィジェット(iOS 17+)

WidgetConfigurationIntent

ウィジェットの設定パラメーターを定義します。

import AppIntents

struct SelectProjectIntent: WidgetConfigurationIntent {
    static var title: LocalizedStringResource = "Select Project"
    static var description = IntentDescription("Choose which project to display")

    @Parameter(title: "Project")
    var project: ProjectEntity?

    // Provide default value
    static var parameterSummary: some ParameterSummary {
        Summary("Show \(\.$project)")
    }
}

EntityとEntityQuery

設定用の動的なオプションを提供します。

struct ProjectEntity: AppEntity {
    var id: String
    var name: String

    static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Project")

    var displayRepresentation: DisplayRepresentation {
        DisplayRepresentation(title: "\(name)")
    }
}

struct ProjectQuery: EntityQuery {
    func entities(for identifiers: [String]) async throws -> [ProjectEntity] {
        // Return projects matching these IDs
        return await ProjectStore.shared.projects(withIDs: identifiers)
    }

    func suggestedEntities() async throws -> [ProjectEntity] {
        // Return all available projects
        return await ProjectStore.shared.allProjects()
    }
}

Providerでの設定の使用

struct Provider: AppIntentTimelineProvider {
    func timeline(for configuration: SelectProjectIntent, in context: Context) async -> Timeline<SimpleEntry> {
        let project = configuration.project // Use selected project
        let entries = await generateEntries(for: project)
        return Timeline(entries: entries, policy: .atEnd)
    }
}

パート 4:ライブアクティビティ(iOS 16.1+)

ActivityAttributes

ライブアクティビティの静的データと動的データを定義します。

import ActivityKit

struct PizzaDeliveryAttributes: ActivityAttributes {
    // Static data - set when activity starts, never changes
    struct ContentState: Codable, Hashable {
        // Dynamic data - updated throughout activity lifecycle
        var status: DeliveryStatus
        var estimatedDeliveryTime: Date
        var driverName: String?
    }

    // Static attributes
    var orderNumber: String
    var pizzaType: String
}

主な制約:正常に開始するには、ActivityAttributesの合計データサイズが4KB未満である必要があります。

アクティビティの開始

認証のリクエスト
import ActivityKit

let authorizationInfo = ActivityAuthorizationInfo()
let areActivitiesEnabled = authorizationInfo.areActivitiesEnabled
アクティビティの開始
let attributes = PizzaDeliveryAttributes(
    orderNumber: "12345",
    pizzaType: "Pepperoni"
)

let initialState = PizzaDeliveryAttributes.ContentState(
    status: .preparing,
    estimatedDeliveryTime: Date().addingTimeInterval(30 * 60)
)

let activity = try Activity.request(
    attributes: attributes,
    content: ActivityContent(state: initialState, staleDate: nil),
    pushType: nil // or .token for push notifications
)

エラー処理

よくあるアクティビティエラー

リクエストの前に、必ずActivityAuthorizationInfo().areActivitiesEnabledを確認してください。Activity.request()から発生する次のエラーを処理してください。

  • ActivityAuthorizationError — ユーザーがライブアクティビティの許可を拒否しました
  • ActivityError.dataTooLarge — ActivityAttributesが4KBを超えています。属性のサイズを小さくしてください
  • ActivityError.tooManyActivities — システムの上限に達しました(通常、同時に2~3件)

リクエストが成功したら、後で更新できるようにactivity.idを保存します。

アクティビティの更新

新しいコンテンツで更新
// Find active activity by stored ID
guard let activity = Activity<PizzaDeliveryAttributes>.activities
    .first(where: { $0.id == storedActivityID }) else { return }

let updatedState = PizzaDeliveryAttributes.ContentState(
    status: .onTheWay,
    estimatedDeliveryTime: Date().addingTimeInterval(10 * 60),
    driverName: "John"
)

await activity.update(
    ActivityContent(
        state: updatedState,
        staleDate: Date().addingTimeInterval(60) // Mark stale after 1 min
    )
)
アラートの設定
await activity.update(updatedContent, alertConfiguration: AlertConfiguration(
    title: "Pizza is here!",
    body: "Your \(attributes.pizzaType) pizza has arrived",
    sound: .default
))
アクティビティのライフサイクルの監視

activity.activityStateUpdatesの非同期シーケンスを使用して、状態の変化(.active、.ended、.dismissed、.stale)を監視します。.endedまたは.dismissedになったら、保存したアクティビティIDをクリーンアップします。deinitで監視タスクをキャンセルします。

アクティビティの終了

非表示ポリシー
await activity.end(
    ActivityContent(state: finalState, staleDate: nil),
    dismissalPolicy: .default
)

非表示ポリシーのオプション:

  • .immediate — 即座に削除します
  • .default — ロック画面に約4時間表示されます
  • .after(date) — 指定した時刻に削除します(例:.after(Date().addingTimeInterval(3600)))

ライブアクティビティのプッシュ通知

プッシュトークンのリクエスト
let activity = try Activity.request(
    attributes: attributes,
    content: initialContent,
    pushType: .token // Request push token
)

// Monitor for push token
for await pushToken in activity.pushTokenUpdates {
    let tokenString = pushToken.map { String(format: "%02x", $0) }.joined()
    // Send to your server
    await sendTokenToServer(tokenString, activityID: activity.id)
}
頻繁なプッシュ更新(iOS 18.2+)

標準の上限は1時間あたり約10~12回のプッシュです(スポーツや株価などの)ライブイベントでは、com.apple.developer.activity-push-notification-frequent-updatesエンタイトルメントを追加すると、上限を大幅に引き上げられます。


パート 5:Dynamic Island(iOS 16.1+)

表示タイプ

ライブアクティビティは、Dynamic Islandに3つのサイズクラスで表示されます。

コンパクト(Leading + Trailing)

別のライブアクティビティが展開されている場合、または複数のアクティビティが有効な場合に表示されます。

DynamicIsland {
    DynamicIslandExpandedRegion(.leading) {
        Image(systemName: "timer")
    }
    DynamicIslandExpandedRegion(.trailing) {
        Text("\(entry.timeRemaining)")
    }
    // ...
} compactLeading: {
    Image(systemName: "timer")
} compactTrailing: {
    Text("\(entry.timeRemaining)")
        .frame(width: 40)
}
ミニマル

有効なライブアクティビティが2件を超える場合に表示されます(円形のアバター)。

DynamicIsland {
    // ...
} minimal: {
    Image(systemName: "timer")
        .foregroundStyle(.tint)
}
展開

ユーザーがコンパクト表示を長押しすると表示されます。

DynamicIsland {
    DynamicIslandExpandedRegion(.leading) {
        Image(systemName: "timer")
            .font(.title)
    }

    DynamicIslandExpandedRegion(.trailing) {
        VStack(alignment: .trailing) {
            Text("\(entry.timeRemaining)")
                .font(.title2.monospacedDigit())
            Text("remaining")
                .font(.caption)
        }
    }

    DynamicIslandExpandedRegion(.center) {
        // Optional center content
    }

    DynamicIslandExpandedRegion(.bottom) {
        HStack {
            Button(intent: PauseIntent()) {
                Label("Pause", systemImage: "pause.fill")
            }
            Button(intent: StopIntent()) {
                Label("Stop", systemImage: "stop.fill")
            }
        }
    }
}

デザイン原則(WWDC 2023-10194より)

同心円状の配置

コンテンツは均等な余白を保ち、Dynamic Islandの丸みを帯びた形状の内側に同心円状に収まるようにします。Circle()またはRoundedRectangle(cornerRadius:)を使用します。角に食い込む鋭角のRectangle()は使用しないでください。

生物のような動き

Dynamic Islandのアニメーションは、自然で弾力のある動きにします。線形アニメーションではなく、.spring(response: 0.6, dampingFraction: 0.7)または.interpolatingSpring(stiffness: 300, damping: 25)を使用します。


パート 6:コントロールセンターウィジェット(iOS 18+)

ControlWidgetプロトコル

コントロールは、コントロールセンター、ロック画面、アクションボタン(iPhone 15 Pro+)に表示されます。

StaticControlConfiguration

設定を必要としないシンプルなコントロール向けです。

import WidgetKit
import AppIntents

struct TorchControl: ControlWidget {
    var body: some ControlWidgetConfiguration {
        StaticControlConfiguration(kind: "TorchControl") {
            ControlWidgetButton(action: ToggleTorchIntent()) {
                Label("Flashlight", systemImage: "flashlight.on.fill")
            }
        }
        .displayName("Flashlight")
        .description("Toggle flashlight")
    }
}
AppIntentControlConfiguration

設定可能なコントロール向けです。

struct TimerControl: ControlWidget {
    var body: some ControlWidgetConfiguration {
        AppIntentControlConfiguration(
            kind: "TimerControl",
            intent: ConfigureTimerIntent.self
        ) { configuration in
            ControlWidgetButton(action: StartTimerIntent(duration: configuration.duration)) {
                Label("\(configuration.duration)m Timer", systemImage: "timer")
            }
        }
    }
}

ControlWidgetButton

単発の操作など、個別のアクション向けです。

ControlWidgetButton(action: PlayMusicIntent()) {
    Label("Play", systemImage: "play.fill")
}
.tint(.purple)

ControlWidgetToggle

真偽値の状態向けです。

struct AirplaneModeControl: ControlWidget {
    var body: some ControlWidgetConfiguration {
        StaticControlConfiguration(kind: "AirplaneModeControl") {
            ControlWidgetToggle(
                isOn: AirplaneModeIntent.isEnabled,
                action: AirplaneModeIntent()
            ) { isOn in
                Label(isOn ? "On" : "Off", systemImage: "airplane")
            }
        }
    }
}

値プロバイダー(非同期の状態)

非同期の状態が必要なコントロールでは、ControlValueProviderをStaticControlConfigurationに渡します。

struct ThermostatProvider: ControlValueProvider {
    func currentValue() async throws -> ThermostatValue {
        let temp = try await HomeManager.shared.currentTemperature()
        return ThermostatValue(temperature: temp)
    }
    var previewValue: ThermostatValue { ThermostatValue(temperature: 72) }
}

プロバイダーの値は、コントロールのクロージャに渡されます:{ value in ControlWidgetButton(...)}。

設定可能なコントロール

WidgetConfigurationIntent を指定して AppIntentControlConfiguration を使用します(設定可能なウィジェットと同じパターンです)。コントロールを追加する際に設定 UI を表示するには、.promptsForUserConfiguration() を追加します。

コントロールの詳細設定

  • .controlWidgetActionHint("Toggles flashlight") — VoiceOver のアクセシビリティヒント
  • .displayName("My Control") / .description("...") — コントロールセンターの UI に表示されます

パート 7:iOS 18 以降のアップデート

アクセント付きレンダリングと Liquid Glass

ウィジェットのレンダリングモードは、複数の iOS バージョンにまたがります。widgetAccentable()(iOS 16 以降)、WidgetAccentedRenderingMode(iOS 18 以降)、glassEffect() や GlassEffectContainer などの Liquid Glass エフェクト(iOS 26 以降)があります。モードを検出し、それに応じてレイアウトを調整してください。

レンダリングモードの検出
struct MyWidgetView: View {
    @Environment(\.widgetRenderingMode) var renderingMode

    var body: some View {
        if renderingMode == .accented {
            // Simplified layout — opaque images tinted white, background replaced with glass
        } else {
            // Standard full-color layout
        }
    }
}
widgetAccentable(_:)

ビューをアクセントグループの一部として指定します。アクセント付きモードでは、アクセントグループのビューはプライマリグループのビューとは別に色付けされ、視覚的な階層が生まれます。

HStack {
    VStack(alignment: .leading) {
        Text("Title")
            .font(.headline)
            .widgetAccentable()  // Accent group — tinted in accented mode
        Text("Subtitle")
            // Primary group by default
    }
    Image(systemName: "star.fill")
        .widgetAccentable()  // Also accent group
}
WidgetAccentedRenderingMode

アクセント付きモードでの画像のレンダリング方法を制御します。Image ビューに適用します。

Image("myPhoto")
    .widgetAccentedRenderingMode(.accented)      // Tinted with accent color
Image("myIcon")
    .widgetAccentedRenderingMode(.monochrome)     // Rendered as monochrome
Image("myBadge")
    .widgetAccentedRenderingMode(.fullColor)       // Keeps original colors (opt-out)

ベストプラクティス:フルカラー画像は .fullColor レンダリングモードでのみ表示してください。視覚的な階層を作るために、.widgetAccentable() を適切に使用してください。複数のアクセントカラーと背景画像でテストしてください。

コンテナの背景
VStack { /* content */ }
    .containerBackground(for: .widget) {
        Color.blue.opacity(0.2)
    }

アクセント付きモードでは、システムが背景を削除し、テーマに合わせたガラスに置き換えます。削除を防ぐには(ウィジェットは iPad のロック画面と StandBy では表示されなくなります):

.containerBackgroundRemovable(false)
カスタムウィジェット要素の Liquid Glass
Text("Label")
    .padding()
    .glassEffect()  // Default capsule shape

Image(systemName: "star.fill")
    .frame(width: 60, height: 60)
    .glassEffect(.regular, in: .rect(cornerRadius: 12))

Button("Action") { }
    .buttonStyle(.glass)

複数のガラス要素を GlassEffectContainer と組み合わせます。

GlassEffectContainer(spacing: 20.0) {
    HStack(spacing: 20.0) {
        Image(systemName: "cloud")
            .frame(width: 60, height: 60)
            .glassEffect()
        Image(systemName: "sun")
            .frame(width: 60, height: 60)
            .glassEffect()
    }
}

クロスプラットフォーム対応

visionOS ウィジェット(visionOS 2 以降)

visionOS ウィジェットは、物理空間に配置される 3D オブジェクトです。面に取り付けたり、浮かべたりできます。独自の空間機能に対応しています。

取り付けスタイル

ウィジェットは、面の上に配置する(高くする)か、壁などの垂直面に埋め込む(くぼませる)ことができます。

.supportedMountingStyles([.elevated, .recessed])  // Default is both
// .supportedMountingStyles([.recessed])           // Wall-only widget

.recessed のみを指定すると、ユーザーはウィジェットを水平面に配置できません。

ウィジェットのテクスチャ

空間表示に使える 2 種類のビジュアルテクスチャ:

.widgetTexture(.glass)   // Default — transparent glass-like appearance
.widgetTexture(.paper)   // Poster-like look, effective with extra-large sizes
近接認識(levelOfDetail)

ウィジェットはユーザーとの距離に応じて自動的に調整されます。システムは詳細レベル間の遷移をアニメーション表示します。

@Environment(\.levelOfDetail) var levelOfDetail

var body: some View {
    VStack {
        Text(entry.value)
            .font(levelOfDetail == .simplified ? .largeTitle : .title)
    }
}

値:.default(近くで見る場合)と .simplified(離れて見る場合。文字を大きくし、詳細を減らします)。

visionOS ウィジェットファミリー

visionOS は、特大サイズを含むすべてのシステムファミリーに対応しています。

.supportedFamilies([
    .systemSmall, .systemMedium, .systemLarge,
    .systemExtraLarge,
    .systemExtraLargePortrait  // visionOS-specific portrait orientation
])

ポスターのような表示には、特大サイズのファミリーと .widgetTexture(.paper) の組み合わせが特に効果的です。

背景の検出

ウィジェットの背景が表示されているかどうかを検出します(アクセント付きモードでは削除されます)。

@Environment(\.showsWidgetContainerBackground) var showsBackground
CarPlay(iOS 18 以降)

ActivityConfiguration に .supplementalActivityFamilies([.medium]) を追加します。StandBy 形式の全幅ダッシュボード表示を使用します。

macOS のメニューバー

ペアリングされた iPhone のライブアクティビティは、macOS Sequoia 以降のメニューバーに自動的に表示されます。コードの変更は不要です。

watchOS のコントロール(11 以降)

ControlWidget は watchOS でも同じように機能し、コントロールセンター、アクションボタン、Smart Stack で使用できます。iOS と同じ StaticControlConfiguration / ControlWidgetButton パターンを使います。

関連性ウィジェット(iOS 18 以降)

.relevanceConfiguration(for:score:attributes:) を使って、Smart Stack でウィジェットが優先的に表示されるようシステムに伝えます。属性には、コンテキストに応じた順位付けに使う .location(CLLocation)、.timeOfDay(DateInterval)、.activity(String) があります。

プッシュ通知による更新(iOS 18 以降)

PKPushRegistryDelegate を実装し、.widgetKit のプッシュタイプを処理して、サーバーからウィジェットへのプッシュ通知を受信します。共有コンテナのデータを更新し、WidgetCenter.shared.reloadAllTimelines() を呼び出します。iPhone へのプッシュ通知は、Apple Watch と CarPlay にも自動的に同期されます。


パート 8:App Groups とデータ共有

App Groups のエンタイトルメント

アプリと拡張機能の間でデータを共有するために必要です。

設定
  1. Xcode:ターゲット → Signing & Capabilities → 「App Groups」を追加
  2. 識別子の形式:group.com.company.appname
  3. メインアプリのターゲットと拡張機能のターゲットの両方で有効にします

共有コンテナ

共有コンテナへのアクセス
let sharedContainer = FileManager.default.containerURL(
    forSecurityApplicationGroupIdentifier: "group.com.mycompany.myapp"
)!

let dataFileURL = sharedContainer.appendingPathComponent("widgetData.json")
App Groups での UserDefaults
// Main app - write data
let shared = UserDefaults(suiteName: "group.com.mycompany.myapp")!
shared.set("Updated value", forKey: "myKey")

// Widget extension - read data
let shared = UserDefaults(suiteName: "group.com.mycompany.myapp")!
let value = shared.string(forKey: "myKey")
App Groups での Core Data

NSPersistentStoreDescription の保存先に共有コンテナの URL を指定します:

let sharedStoreURL = FileManager.default.containerURL(
    forSecurityApplicationGroupIdentifier: "group.com.mycompany.myapp"
)!.appendingPathComponent("MyApp.sqlite")

let description = NSPersistentStoreDescription(url: sharedStoreURL)
container.persistentStoreDescriptions = [description]

IPC 通信

  • バックグラウンド URL セッション — 拡張機能からアクセスできるダウンロードを行うには、config.sharedContainerIdentifier に App Group ID を設定します
  • Darwin 通知センター — CFNotificationCenterGetDarwinNotifyCenter() とともに CFNotificationCenterPostNotification / CFNotificationCenterAddObserver を使用して、プロセス間で簡単なシグナルを送信します(例:ウィジェットに WidgetCenter.shared.reloadAllTimelines() の呼び出しを通知します)

パート 9:watchOS との統合

supplementalActivityFamilies(watchOS 11 以降)

ActivityConfiguration に .supplementalActivityFamilies([.small]) を追加すると、Apple Watch のスマートスタックにライブアクティビティを表示できます(.medium を使う CarPlay と同じモディファイアです)。

activityFamily 環境値

@Environment(\.activityFamily) を使ってレイアウトを調整します。.small(watchOS)か iPhone のレイアウトかを確認してください。

常時表示ディスプレイ

常時表示ディスプレイ用にビューを簡素化するには、@Environment(\.isLuminanceReduced) を使います。詳細を減らし、白いテキストと大きめのフォントを使用します。適切にダークモードを処理するには、@Environment(\.colorScheme) と組み合わせます。

更新の割り当て(watchOS)

watchOS の更新は、プッシュ通知を通じて iPhone と自動的に同期されます。Apple Watch が Bluetooth の通信範囲外にある場合、更新が遅れることがあります。


パート 10:実践的なワークフロー

最初のウィジェットを作成する

動作するコード例を含む、手順を追った完全なチュートリアルについては、Apple の WidgetKit と SwiftUI を使ったウィジェットの作成 サンプルプロジェクトを参照してください。

主な手順:ウィジェット拡張機能のターゲットを追加し、App Groups を設定して、TimelineProvider を実装し、SwiftUI ビューを設計し、メインアプリから更新します。本番環境で必要となる要件については、以下のエキスパートレビュー用チェックリストを参照してください。


エキスパートレビュー用チェックリスト

ウィジェットを出荷する前に

アーキテクチャ:

  • App Groups エンタイトルメントをアプリとエクステンションの両方で設定する
  • グループ識別子が両方のターゲットで完全に一致している
  • データ共有にはすべて共有コンテナを使用する
  • ウィジェットのコードで UserDefaults.standard を使用しない

パフォーマンス:

  • タイムラインの生成が 5 秒未満で完了する
  • ウィジェットのビューでネットワークリクエストを実行しない
  • タイムラインの更新間隔が適切である(≥ 15 min)
  • エントリー数が適切である(< 20-30 entries)
  • メモリ使用量が上限内である(ウィジェットは約 30MB、アクティビティは約 50MB)
  • 画像が最適化されている(アセットカタログまたは SF Symbols を推奨)

データと状態:

  • ウィジェットがデータの欠落や nil を適切に処理する
  • エントリーの日付が時系列順になっている
  • プレースホルダービューが適切に表示される
  • スナップショットビューが実際の使用状況を代表している

ユーザーエクスペリエンス:

  • ウィジェットがウィジェットギャラリーに表示される
  • configurationDisplayName が明確で簡潔である
  • 説明にウィジェットの目的が記載されている
  • サポートするすべてのファミリーをテストし、正しく表示されることを確認している
  • 明るい背景と暗い背景の両方でテキストが読みやすい
  • インタラクティブ要素(ボタン/トグル)が正しく動作する

ライブアクティビティ(該当する場合):

  • ActivityAttributes が 4KB 未満である
  • 開始前に認証を確認する
  • イベントの完了時にアクティビティが終了する
  • 適切な却下ポリシーを設定する
  • 該当する場合は watchOS サポートを設定する(supplementalActivityFamilies)
  • Dynamic Island のレイアウト(コンパクト、最小、展開)をテストする

Liquid Glass(該当する場合):

  • アクセントモードで視覚的な階層を作るために widgetAccentable() を適用する
  • 画像に WidgetAccentedRenderingMode(.accented、.monochrome、または .fullColor)を設定する
  • 複数のアクセントカラーと背景画像でテストする
  • コンテナの背景に .containerBackground(for: .widget) を設定する

visionOS(該当する場合):

  • マウントスタイル(.elevated、.recessed、または両方)を設定する
  • ウィジェットのテクスチャ(.glass または .paper)を選択する
  • 距離に応じたレイアウトのために levelOfDetail を処理する
  • 適切な場合は特大ファミリー(.systemExtraLarge、.systemExtraLargePortrait)をサポートする
  • 距離を変えて近接による切り替えをテストする

コントロールセンターウィジェット(該当する場合):

  • ControlValueProvider が非同期で高速に動作する(< 1 second)
  • previewValue が適切なフォールバックを提供する
  • displayName と description を設定する
  • コントロールセンター、ロック画面、アクションボタンでテストする

テスト:

  • 実機でテストする(シミュレーターだけで済ませない)
  • ウィジェットの追加と削除をテストする
  • アプリのデータ変更後にウィジェットが更新されることをテストする
  • アプリを強制終了してもウィジェットが動作することをテストする
  • メモリ不足のシナリオをテストする
  • サポートするすべての iOS バージョンでテストする
  • インターネット接続がない状態でテストする

テストのガイダンス

単体テストのパターン

placeholder()、getSnapshot()、getTimeline() メソッドをテストします。テストデータを共有コンテナに保存し、モックコンテキストを指定して getTimeline() を呼び出し、エントリーが空でなく、期待されるデータが含まれていることを検証します。非同期のタイムライン生成には waitForExpectations(timeout: 5.0) を使用します。

手動テストのチェックリスト
  • ウィジェットをホーム画面に追加し、ウィジェットギャラリー、サポートするすべてのサイズ、アプリとのデータ一致を確認する
  • メインアプリでデータを変更してウィジェットの更新を確認し、アプリを強制終了してからデバイスを再起動する
  • アプリのデータをすべて削除する(適切な処理)、ネットワークを無効にする(オフライン)、低電力モード、複数インスタンスをテストする
  • Xcode Debug Navigator でメモリを監視し、Console でタイムライン生成時間を確認し、古いデバイスでテストする
デバッグのヒント
  • getTimeline() が呼び出され、データが読み込まれていることを確認するため、getTimeline() に print() ログを追加する
  • App Groups を確認する:アプリとウィジェットの両方で FileManager.default.containerURL(forSecurityApplicationGroupIdentifier:) を出力し、パスが一致することを確認する
  • メインアプリでデータを変更した後、WidgetCenter.shared.reloadAllTimelines() を呼び出す

パート 11:トラブルシューティング

ギャラリーにウィジェットが表示されない:WidgetBundle にウィジェットが含まれていることを確認し、supportedFamilies() を確認します。拡張機能の「Skip Install」が NO になっていること、デプロイメントターゲットがアプリと一致していることを確認します。

ウィジェットが更新されない

症状:ウィジェットに古いデータが表示され、更新されません。

診断手順:

  1. タイムラインポリシーを確認します(.atEnd、.after()、.never のいずれか)。
  2. 1 日あたりの予算(40~70 回の再読み込み)を超えていないことを確認します。
  3. getTimeline() が呼び出されているか確認します(ログを追加します)。
  4. 共有データ用の App Groups が正しく設定されていることを確認します。

解決策:

// Manual reload from main app when data changes
import WidgetKit

WidgetCenter.shared.reloadAllTimelines()
// or
WidgetCenter.shared.reloadTimelines(ofKind: "MyWidget")

アプリとウィジェットの間でデータが共有されない

症状:ウィジェットにデフォルトデータまたは空のデータが表示されます。

診断手順:

  1. 両方のターゲットで App Groups のエンタイトルメントを確認します。
  2. グループ識別子が完全に一致していることを確認します。
  3. 両方のターゲットで同じ suiteName を使用していることを確認します。
  4. 共有コンテナを使用している場合は、ファイルパスを確認します。

解決策:

// Both app AND extension must use:
let shared = UserDefaults(suiteName: "group.com.mycompany.myapp")!

// NOT:
let shared = UserDefaults.standard  // ❌ Different containers

Live Activity が開始しない

症状:Activity.request() がエラーをスローします。

一般的なエラー:

「Activity のサイズが 4KB を超えています」:

// ❌ BAD: Large images in attributes
struct MyAttributes: ActivityAttributes {
    var productImage: UIImage  // Too large!
}

// ✅ GOOD: Use asset catalog names
struct MyAttributes: ActivityAttributes {
    var productImageName: String  // Reference to asset
}

「Activities が有効になっていません」:

// Check authorization first
let authInfo = ActivityAuthorizationInfo()
guard authInfo.areActivitiesEnabled else {
    throw ActivityError.notEnabled
}

インタラクティブウィジェットのボタンが動作しない

症状:ボタンをタップしても何も起きません。

診断手順:

  1. App Intent の perform() が IntentResult を返すことを確認します。
  2. ウィジェットターゲットに intent がインポートされていることを確認します。
  3. ボタンで action: ではなく intent: パラメーターが使用されていることを確認します。
  4. Console で intent の実行エラーを確認します。

解決策:

// ✅ CORRECT: Use intent parameter
Button(intent: MyIntent()) {
    Label("Action", systemImage: "star")
}

// ❌ WRONG: Don't use action closure
Button(action: { /* This won't work in widgets */ }) {
    Label("Action", systemImage: "star")
}

コントロールセンターのウィジェットの動作が遅い:ControlValueProvider.currentValue() では async を使用し、Thread.sleep で処理をブロックしないでください。高速な previewValue フォールバックを用意します。

ウィジェットのサイズが正しくない:ビューで @Environment(\.widgetFamily) に応じて切り替え、ファミリーごとにレイアウトを調整し、サイズのハードコードを避けます。

タイムラインエントリーの順序が正しくない:エントリーの日付が時系列順になっていることを確認します。Date() を基準にオフセットを増やしていきます。

watchOS の Live Activity が表示されない:ActivityConfiguration に .supplementalActivityFamilies([.small]) を追加し、watchOS 11 以降であることを確認します。また、Bluetooth とペアリングの状態も確認します。

パフォーマンスの問題

症状:ウィジェットの描画が遅く、バッテリーを消耗します。

一般的な原因:

  • タイムラインエントリーが多すぎます(> 100)。
  • ビューコード内でネットワークリクエストを実行しています。
  • getTimeline() 内で負荷の高い計算を行っています。
  • 更新間隔が短すぎます(< 15 分)。

解決策:

// ✅ GOOD: Strategic intervals
let entries = (0..<8).map { offset in
    let date = Calendar.current.date(byAdding: .minute, value: offset * 15, to: now)!
    return SimpleEntry(date: date, data: precomputedData)
}

// ❌ BAD: Too frequent, too many entries
let entries = (0..<100).map { offset in
    let date = Calendar.current.date(byAdding: .minute, value: offset, to: now)!
    return SimpleEntry(date: date, data: fetchFromNetwork())  // Network in timeline
}

ウィジェットのデバッグ

シミュレーターとデバイス
  • シミュレーター:ウィジェットはすぐに更新され、予算制限も適用されません。レイアウトのテストには便利ですが、更新動作の確認には適しません。
  • デバイス:予算の制限があります(1 日あたり 40~70 回の再読み込み)。実際の更新タイミングを確認するため、リリース前にデバイスでテストしてください。
  • Xcode Previews:レイアウトには使えますが、getTimeline() は実行されません。タイムラインのロジックは単体テストまたはデバイスでの実行でテストしてください。
一般的なデバッグのワークフロー
  1. getTimeline() に print() を追加し、呼び出されてデータが読み込まれることを確認します。
  2. ウィジェット拡張機能のプロセス名でフィルタリングして Console.app を確認します。
  3. WidgetCenter.shared.getCurrentConfigurations() を使って登録を確認します。
  4. アプリの更新後もウィジェットに古いデータが表示される場合は、App Groups のコンテナパスが一致していることを確認します。
データ共有のパターン

ウィジェットでの SwiftData(iOS 17 以降):

  • ウィジェットでメインアプリと同じスキーマの ModelContainer を作成します。
  • 共有 App Groups コンテナを使用します:ModelConfiguration(url: containerURL)
  • ウィジェットからは読み取りのみを行い、競合を避けるため書き込みは行いません。
  • メインアプリで書き込みを行った後、WidgetCenter.shared.reloadAllTimelines() を呼び出します。

ウィジェットでの GRDB/SQLite:

  • App Groups コンテナを介してデータベースファイルを共有します。
  • 並行読み取りには DatabaseQueue ではなく DatabasePool を使用します。
  • ウィジェットでは読み取り専用の接続を開きます:try DatabasePool(path: dbPath, configuration: readOnlyConfig)
  • 誤って書き込まないよう、ウィジェットで configuration.readonly = true を設定します。

リソース

WWDC:2025-278, 2024-10157, 2024-10068, 2024-10098, 2023-10028, 2023-10194, 2022-10184, 2022-10185

ドキュメント:/widgetkit, /activitykit, /appintents

スキル:axiom-app-intents-ref, axiom-swift-concurrency, axiom-swiftui-performance, axiom-swiftui-layout, axiom-extensions-widgets


バージョン:0.9 | プラットフォーム:iOS 14+, iPadOS 14+, watchOS 9+, macOS 11+, visionOS 2+

コメント