axiom-extensions-widgets-ref
iOS 14以降でウィジェット、ライブアクティビティ、コントロールセンターコントロール、またはアプリ拡張機能を実装する際に使用します。WidgetKit、ActivityKit、App Groups、および拡張機能のライフサイクルに関する包括的なAPIリファレンスを提供します。
description の原文を見る
Use when implementing widgets, Live Activities, Control Center controls, or app extensions - comprehensive API reference for WidgetKit, ActivityKit, App Groups, and extension lifecycle for iOS 14+
SKILL.md 本文
Extensions & Widgets API Reference
Overview
Apple のウィジェットと拡張機能エコシステムの包括的な API リファレンスを提供します:
- 標準ウィジェット (iOS 14+) — ホーム画面、ロック画面、StandBy ウィジェット
- インタラクティブウィジェット (iOS 17+) — App Intents を使用したボタンとトグル
- Live Activities (iOS 16.1+) — ロック画面と Dynamic Island のリアルタイム更新
- Control Center ウィジェット (iOS 18+) — システム全体のクイックコントロール
- アプリ拡張機能 — データ共有、ライフサイクル、エンタイトルメント
ウィジェットは SwiftUI の アーカイブされたスナップショット であり、システムによってタイムライン上でレンダリングされます。拡張機能はアプリにバンドルされたサンドボックス化された実行ファイルです。
このスキルを使用する場合
✅ このスキルを使用してください:
- あらゆるタイプのウィジェット (ホーム画面、ロック画面、StandBy) を実装する場合
- 進行中のイベントの Live Activities を作成する場合
- Control Center コントロールを構築する場合
- アプリと拡張機能の間でデータを共有する場合
- ウィジェットのタイムラインと更新ポリシーを理解する場合
- ウィジェットを App Intents と統合する場合
- watchOS または visionOS ウィジェットをサポートする場合
❌ このスキルを使用しないでください:
- App Intents に関する質問 (app-intents-ref スキルを使用)
- SwiftUI レイアウトの問題 (swiftui-layout スキルを使用)
- パフォーマンス最適化 (swiftui-performance スキルを使用)
- クラッシュのデバッグ (xcode-debugging スキルを使用)
関連スキル
- app-intents-ref — インタラクティブウィジェットとコンフィグレーション用の App Intents
- swift-concurrency — ウィジェットデータロード用の async/await パターン
- swiftui-performance — ウィジェットレンダリングの最適化
- swiftui-layout — 複雑なウィジェットレイアウト
- extensions-widgets — アンチパターンとデバッグを含むディシプリンスキル
主要用語
- タイムライン — コンテンツをいつ何を表示するかを定義する一連のエントリ。システムは指定された時刻にエントリを表示します
- TimelineProvider — タイムラインエントリ (プレースホルダー、スナップショット、タイムライン生成) を提供するプロトコル
- TimelineEntry — ウィジェットデータ + 表示日時を含む構造体
- タイムラインバジェット — タイムラインリロードの1日の上限 (40-70)
- バジェット除外 — リロードカウントに含まれない再読み込み (ユーザー開始、アプリフォアグラウンド化、システム開始)
- ウィジェットファミリー — サイズ/形状 (systemSmall、systemMedium、accessoryCircular など)
- App Groups — アプリと拡張機能間の共有データコンテナーのエンタイトルメント
- ActivityAttributes — 静的データ (1回設定) + 動的 ContentState (ライフサイクル中に更新)
- ContentState — ActivityAttributes の変更部分。合計 4KB 未満である必要があります
- Dynamic Island — iPhone 14 Pro+ の Live Activity 表示。コンパクト、ミニマル、拡張サイズ
- ControlWidget — iOS 18+ の Control Center、ロック画面、アクションボタン用ウィジェット
- 補足 Activity ファミリー — Apple Watch または CarPlay での Live Activities を有効化
Part 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
Live Activities 用 (Live Activities セクションで取り扱い)。
適切な設定を選択する
ユーザー設定が不要ですか?StaticConfiguration を使用してください。シンプルな静的オプションですか?AppIntentConfiguration と WidgetConfigurationIntent を使用してください。アプリデータからの動的オプションですか?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(単一行) — 日付の上、テキストのみ
例: 複数ファミリーのサポート
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 {
// 読み込み中のプレースホルダー
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), emoji: "😀")
}
// ウィジェットギャラリーに表示
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), emoji: "📷")
completion(entry)
}
// 実際のタイムライン
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
let currentDate = Date()
// 5 時間ごとに 1 エントリを作成
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
// このタイプのすべてのウィジェットを再読み込み
WidgetCenter.shared.reloadAllTimelines()
// 特定のタイプを再読み込み
WidgetCenter.shared.reloadTimelines(ofKind: "MyWidget")
パフォーマンスとバジェットのクイックリファレンス
タイムライン更新バジェット
- 1 日のバジェット: 40-70 回/日 (システム負荷とエンゲージメントにより異なる)
- バジェット除外: ユーザー開始の再読み込み、アプリフォアグラウンド化、ウィジェット追加、システム再起動
- 戦略的 (4 回/時間) — 約 48 回/日、低バッテリー影響
- 積極的 (12 回/時間) — 18:00 までにバジェット枯渇、高影響
- オンデマンドのみ — 5-10 回/日、最小限の影響
- 重要なデータ変更と時間ベースのイベントで再読み込みします。推測または装飾的な再読み込みは避けてください。
// ✅ GOOD: 戦略的間隔 (15-60 分)
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はウィジェットで動作しません- 大きな画像はメモリ終了を引き起こします
Part 2: インタラクティブウィジェット (iOS 17+)
ボタンとトグル
インタラクティブウィジェットは SwiftUI Button と Toggle を App Intents と共に使用します。
App Intent を備えたボタン
Button(intent: IncrementIntent()) {
Label("Increment", systemImage: "plus.circle")
}
インテントは perform() メソッド内で App Groups を介して共有データを更新します。完全な AppIntent 定義構文については axiom-app-intents-ref を参照してください。
App Intent を備えたトグル
ボタンと同じパターン — ボタンに Toggle を使用して、変更時にインテントを呼び出します:
Toggle(isOn: $isEnabled) {
Text("Feature")
}
.onChange(of: isEnabled) { newValue in
Task { try? await ToggleFeatureIntent(enabled: newValue).perform() }
}
インテントは @Parameter(title: "Enabled") var enabled: Bool を使用した同じ AppIntent 構造に従います。完全な AppIntent 定義構文については axiom-app-intents-ref を参照してください。
invalidatableContent 修飾子
App Intent 実行中に視覚的フィードバックを提供します。
struct MyWidgetView: View {
var entry: Provider.Entry
var body: some View {
VStack {
Text(entry.status)
.invalidatableContent() // インテント実行中に暗くなる
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)
Part 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?
// デフォルト値を提供
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] {
// これらの ID と一致するプロジェクトを返す
return await ProjectStore.shared.projects(withIDs: identifiers)
}
func suggestedEntities() async throws -> [ProjectEntity] {
// 利用可能なすべてのプロジェクトを返す
return await ProjectStore.shared.allProjects()
}
}
Provider でコンフィグレーションを使用する
struct Provider: AppIntentTimelineProvider {
func timeline(for configuration: SelectProjectIntent, in context: Context) async -> Timeline<SimpleEntry> {
let project = configuration.project // 選択されたプロジェクトを使用
let entries = await generateEntries(for: project)
return Timeline(entries: entries, policy: .atEnd)
}
}
Part 4: Live Activities (iOS 16.1+)
ActivityAttributes
Live Activity の静的データと動的データを定義します。
import ActivityKit
struct PizzaDeliveryAttributes: ActivityAttributes {
// 静的データ - アクティビティ開始時に設定、決して変更されない
struct ContentState: Codable, Hashable {
// 動的データ - アクティビティライフサイクル全体で更新
var status: DeliveryStatus
var estimatedDeliveryTime: Date
var driverName: String?
}
// 静的属性
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 // または push 通知の場合は .token
)
エラーハンドリング
一般的なアクティビティエラー
リクエスト前に常に ActivityAuthorizationInfo().areActivitiesEnabled を確認してください。Activity.request() からこれらのエラーを処理してください:
ActivityAuthorizationError— ユーザーが Live Activities パーミッションを拒否ActivityError.dataTooLarge— ActivityAttributes が 4KB を超過。属性サイズを削減ActivityError.tooManyActivities— システム制限に達成 (通常 2-3 個の同時実行)
成功後は activity.id を保存して後で更新に使用します。
アクティビティの更新
新しいコンテンツで更新
// 保存された 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) // 1 分後に古いと表示
)
)
アラート設定
await activity.update(updatedContent, alertConfiguration: AlertConfiguration(
title: "Pizza is here!",
body: "Your \(attributes.pizzaType) pizza has arrived",
sound: .default
))
アクティビティライフサイクルの監視
activity.activityStateUpdates async シーケンスを使用して状態の変化 (.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)))
Live Activities への Push 通知
Push トークンのリクエスト
let activity = try Activity.request(
attributes: attributes,
content: initialContent,
pushType: .token // Push トークンをリクエスト
)
// Push トークンを監視
for await pushToken in activity.pushTokenUpdates {
let tokenString = pushToken.map { String(format: "%02x", $0) }.joined()
// サーバーに送信
await sendTokenToServer(tokenString, activityID: activity.id)
}
高頻度 Push 更新 (iOS 18.2+)
標準制限は約 10-12 プッシュ/時間です。ライブイベント (スポーツ、株価) 向けに com.apple.developer.activity-push-notification-frequent-updates エンタイトルメントを追加して、大幅に高い制限を取得してください。
Part 5: Dynamic Island (iOS 16.1+)
プレゼンテーションタイプ
Live Activities は Dynamic Island に 3 つのサイズクラスで表示されます:
コンパクト (Leading + Trailing)
別の Live Activity が展開されている場合、または複数のアクティビティがアクティブな場合に表示されます。
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
Image(systemName: "timer")
}
DynamicIslandExpandedRegion(.trailing) {
Text("\(entry.timeRemaining)")
}
// ...
} compactLeading: {
Image(systemName: "timer")
} compactTrailing: {
Text("\(entry.timeRemaining)")
.frame(width: 40)
}
ミニマル
3 個以上の Live Activities がアクティブな場合に表示 (円形アバター)。
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) {
// オプション: センターコンテンツ
}
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) を使用してください。
Part 6: Control Center ウィジェット (iOS 18+)
ControlWidget プロトコル
コントロールは Control Center、ロック画面、アクションボタン (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("...")— Control Center UI に表示
Part 7: iOS 18+ アップデート
Liquid Glass / アクセント レンダリング (iOS 18+)
.widgetAccentedRenderingMode(.accented) をウィジェットビューに適用して、システムガラス効果を取得します。デフォルトは .fullColor です。アクセント時、色はシステムガラスとブレンドします — 複数のコンテキスト (ホーム画面、StandBy、ロック画面) でテストしてください。
クロスプラットフォームサポート
visionOS (2+)
#if os(visionOS) ガード、.supportedFamilies([.systemSmall, .systemMedium])、および空間オーナメント配置用の .ornamentLevel(.default) を使用してください。
CarPlay (iOS 18+)
.supplementalActivityFamilies([.medium]) を ActivityConfiguration に追加してください。StandBy スタイルのフルワイドダッシュボード プレゼンテーションを使用します。
macOS メニューバー
ペアリングされた iPhone の Live Activities は macOS Sequoia+ メニューバーに自動的に表示されます。コード変更は不要です。
watchOS コントロール (11+)
ControlWidget は watchOS でも同じように機能します — Control Center、アクションボタン、Smart Stack で利用可能。iOS と同じ StaticControlConfiguration / ControlWidgetButton パターン。
関連性ウィジェット (iOS 18+)
.relevanceConfiguration(for:score:attributes:) を使用して、Smart Stack でウィジェットを昇格させるようシステムを支援します。属性には .location(CLLocation)、.timeOfDay(DateInterval)、および .activity(String) がコンテキスト認識ランキング用に含まれます。
Push 通知更新 (iOS 18+)
PKPushRegistryDelegate を実装して .widgetKit プッシュタイプを処理し、サーバーからウィジェットへのプッシュを受け取ります。共有コンテナーデータを更新して WidgetCenter.shared.reloadAllTimelines() を呼び出します。iPhone へのプッシュは Apple Watch および CarPlay に自動的に同期します。
Part 8: App Groups とデータ共有
App Groups エンタイトルメント
アプリと拡張機能間のデータ共有に必要。
設定
- Xcode: ターゲット → 署名とケーパビリティ → "App Groups" を追加
- 識別子形式:
group.com.company.appname - メインアプリ ターゲット AND 拡張機能 ターゲット の両方で有効化
共有コンテナー
共有コンテナーにアクセス
let sharedContainer = FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: "group.com.mycompany.myapp"
)!
let dataFileURL = sharedContainer.appendingPathComponent("widgetData.json")
App Groups を使用した UserDefaults
// メインアプリ - データを書き込み
let shared = UserDefaults(suiteName: "group.com.mycompany.myapp")!
shared.set("Updated value", forKey: "myKey")
// ウィジェット拡張機能 - データを読み込み
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 Notification Center —
CFNotificationCenterPostNotification/CFNotificationCenterAddObserverをCFNotificationCenterGetDarwinNotifyCenter()で使用して、シンプルなクロスプロセス信号 (例:ウィジェットにWidgetCenter.shared.reloadAllTimelines()を呼び出すよう通知)
Part 9: watchOS 統合
supplementalActivityFamilies (watchOS 11+)
.supplementalActivityFamilies([.small]) を ActivityConfiguration に追加して、Apple Watch Smart Stack に Live Activities を表示します (CarPlay で .medium として使用される同じ修飾子)。
activityFamily 環境
@Environment(\.activityFamily) を使用してレイアウトを適応させます — .small (watchOS) または iPhone レイアウトを確認してください。
Always On Display
@Environment(\.isLuminanceReduced) を使用して Always On Display のビューを簡略化します — 詳細を削減、白いテキストを使用、大きなフォント。@Environment(\.colorScheme) と組み合わせて、適切なダークモード処理を行います。
更新バジェット (watchOS)
watchOS 更新は push 通知を介して iPhone と自動的に同期します。watch が Bluetooth 範囲外の場合、更新が遅延する可能性があります。
Part 10: 実践的なワークフロー
最初のウィジェットを構築する
完全なステップバイステップチュートリアルと動作コード例については、Apple の Building Widgets Using WidgetKit and SwiftUI サンプルプロジェクトを参照してください。
主要なステップ: ウィジェット拡張機能ターゲットを追加、App Groups を設定、TimelineProvider を実装、SwiftUI ビューを設計、メインアプリから更新します。本番環境要件については以下のエキスパート レビュー チェックリストを参照してください。
エキスパート レビュー チェックリスト
ウィジェット配信前
アーキテクチャ:
- App Groups エンタイトルメントがアプリと拡張機能の両方で設定されている
- グループ識別子が両方のターゲットで正確に一致している
- 共有コンテナーがすべてのデータ共有に使用されている
- ウィジェットコードで
UserDefaults.standardが使用されていない
パフォーマンス:
- タイムライン生成が 5 秒以内に完了
- ウィジェットビューにネットワークリクエストがない
- タイムラインに適切な更新間隔がある (≥ 15 分)
- エントリカウントが合理的 (< 20-30 エントリ)
- メモリ使用量が制限以下 (ウィジェット約 30MB、アクティビティ約 50MB)
- 画像が最適化されている (アセットカタログまたは SF Symbols が推奨)
データと状態:
- ウィジェットが欠落している/nil データを適切に処理している
- エントリの日付が時系列順
- プレースホルダービューが合理的に見える
- スナップショットビューが実際の使用を代表している
ユーザーエクスペリエンス:
- ウィジェットがウィジェットギャラリーに表示される
- configurationDisplayName が明確で簡潔
- description がウィジェットの目的を説明している
- サポートされているすべてのファミリーがテストされ、正しく見える
- テキストが明るい背景と暗い背景の両方で読める
- インタラクティブ要素 (ボタン/トグル) が正常に動作する
Live Activities (該当する場合):
- ActivityAttributes が 4KB 以下
- 開始前に認可が確認されている
- イベント完了時にアクティビティが終了する
- 適切な削除ポリシーが設定されている
- 関連する場合は watchOS サポートが設定されている (supplementalActivityFamilies)
- Dynamic Island レイアウトがテストされている (コンパクト、ミニマル、展開)
Control Center ウィジェット (該当する場合):
- ControlValueProvider 非同期かつ高速 (< 1 秒)
- previewValue が合理的なフォールバックを提供している
- displayName と description が設定されている
- Control Center、ロック画面、アクションボタンでテスト
テスト:
- 実際のデバイスでテスト (シミュレーターのみではない)
- ウィジェットの追加/削除をテスト
- アプリデータ変更 → ウィジェット更新をテスト
- アプリ強制終了 → ウィジェットが動作することをテスト
- 低メモリシナリオをテスト
- サポートするすべての iOS バージョンでテスト
- インターネット接続なしでテスト
テストガイダンス
ユニットテストパターン
placeholder()、getSnapshot()、getTimeline() メソッドをテストします。テストデータを共有コンテナーに保存、モック Context で getTimeline() を呼び出し、エントリが空でなく予期されたデータを含むことをアサートします。非同期タイムライン生成には waitForExpectations(timeout: 5.0) を使用します。
手動テストチェックリスト
- ホーム画面にウィジェットを追加、ウィジェットギャラリーを確認、すべてのサポートサイズ、データがアプリと一致することを確認
- メインアプリのデータを変更、ウィジェット更新を観察、アプリを強制終了、デバイスを再起動
- すべてのアプリデータを削除 (適切な処理)、ネットワークを無効化 (オフライン)、低消費電力モード、複数インスタンス
- Xcode Debug Navigator でメモリを監視、Console でタイムライン生成時間を確認、古いデバイスでテスト
デバッグのコツ
getTimeline()でprint()ログを追加して、呼び出されていることとデータがロードされていることを確認- App Groups を確認:アプリとウィジェットで
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier:)を出力 — パスが一致する必要があります - メインアプリでデータ変更後、
WidgetCenter.shared.reloadAllTimelines()を呼び出します
Part 11: トラブルシューティング
ウィジェットがギャラリーに表示されない: WidgetBundle が含まれていることを確認、supportedFamilies() を確認、拡張機能の "Skip Install" = NO を確認、展開ターゲットがアプリと一致することを確認します。
ウィジェットが更新されない
症状: ウィジェットが古いデータを表示、更新されない
診断手順:
- タイムラインポリシーを確認 (
.atEndvs.after()vs.never) - 1 日のバジェット (40-70 回) を超えていないことを確認
getTimeline()が呼び出されているかどうかを確認 (ログを追加)- App Groups が正しく設定されていることを確認
解決:
// メインアプリのデータ変更時に手動で再読み込み
import WidgetKit
WidgetCenter.shared.reloadAllTimelines()
// または
WidgetCenter.shared.reloadTimelines(ofKind: "MyWidget")
アプリとウィジェット間でデータが共有されない
症状: ウィジェットがデフォルト/空のデータを表示
診断手順:
- App Groups エンタイトルメントが両方のターゲットにあることを確認
- グループ識別子が完全に一致していることを確認
- 両方のターゲットで同じ suiteName を使用していることを確認
- 共有コンテナーを使用している場合はファイルパスを確認
解決:
// アプリと拡張機能の両方が次を使用する必要があります:
let shared = UserDefaults(suiteName: "group.com.mycompany.myapp")!
// NOT:
let shared = UserDefaults.standard // ❌ 別のコンテナー
Live Activity が開始されない
症状: Activity.request() がエラーをスロー
一般的なエラー:
"Activity size exceeds 4KB":
// ❌ BAD: 属性に大きな画像
struct MyAttributes: ActivityAttributes {
var productImage: UIImage // 大きすぎる!
}
// ✅ GOOD: アセットカタログ名を使用
struct MyAttributes: ActivityAttributes {
var productImageName: String // アセットへの参照
}
"Activities not enabled":
// 最初に認可を確認
let authInfo = ActivityAuthorizationInfo()
guard authInfo.areActivitiesEnabled else {
throw ActivityError.notEnabled
}
インタラクティブウィジェットボタンが動作しない
症状: ボタンをタップしても何も起こらない
診断手順:
- App Intent の
perform()がIntentResultを返すことを確認 - インテントがウィジェットターゲットにインポートされていることを確認
- ボタンが
intent:パラメーターを使用していることを確認、action:ではない - Console でインテント実行エラーを確認
解決:
// ✅ CORRECT: intent パラメーターを使用
Button(intent: MyIntent()) {
Label("Action", systemImage: "star")
}
// ❌ WRONG: アクションクロージャーを使用しない
Button(action: { /* これはウィジェットで動作しません */ }) {
Label("Action", systemImage: "star")
}
Control Center ウィジェットが遅い: ControlValueProvider.currentValue() で async を使用、Thread.sleep でブロックしない。高速な previewValue フォールバックを提供します。
ウィジェットが間違ったサイズを表示: ビューで @Environment(\.widgetFamily) をスイッチ、ファミリーごとにレイアウトを適応させ、ハードコードされたサイズを避けます。
タイムラインエントリが順不同: エントリ日付が時系列順であることを確認します。Date() からの増加するオフセットを使用します。
watchOS Live Activity が表示されない: ActivityConfiguration に .supplementalActivityFamilies([.small]) を追加、watchOS 11+ を確認、Bluetooth/ペアリングを確認します。
パフォーマンスの問題
症状: ウィジェットレンダリングが遅い、バッテリードレイン
一般的な原因:
- タイムラインエントリが多すぎる (> 100)
- ビューコードのネットワークリクエスト
getTimeline()での重い計算- 更新間隔が短すぎる (< 15 分)
解決:
// ✅ GOOD: 戦略的な間隔
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: 頻繁すぎる、多すぎるエントリ
let entries = (0..<100).map { offset in
let date = Calendar.current.date(byAdding: .minute, value: offset, to: now)!
return SimpleEntry(date: date, data: fetchFromNetwork()) // タイムラインのネットワーク
}
ウィジェットのデバッグ
シミュレーターとデバイス
- シミュレーター: ウィジェットが即座に更新、バジェット制限なし。レイアウトテストに便利ですが、更新動作に関して誤解を招きます。
- デバイス: バジェット制限付き (1 日 40-70 回)。配信前にデバイスでテストして、実際の更新タイミングを確認します。
- Xcode プレビュー: レイアウトで機能しますが
getTimeline()をスキップします。ユニットテストやデバイス実行でタイムラインロジックをテストします。
一般的なデバッグワークフロー
getTimeline()にprint()を追加 — 呼び出されていることとデータがロードされることを確認- Console.app をウィジェット拡張機能プロセス名でフィルタリング
WidgetCenter.shared.getCurrentConfigurations()を使用して登録を確認- ウィジェットがアプリ更新後に古いデータを表示する場合、App Groups コンテナーパスが一致していることを確認
データ共有パターン
ウィジェット内の SwiftData (iOS 17+):
- ウィジェットで
ModelContainerを作成、メインアプリと同じスキーマ - 共有 App Groups コンテナーを使用:
ModelConfiguration(url: containerURL) - ウィジェット読み取り専用 — ウィジェットからメインアプリへの競合を避けるために書き込まない
- メインアプリ write 後、
WidgetCenter.shared.reloadAllTimelines()を呼び出します
GRDB/SQLite ウィジェット内:
- App Groups コンテナー経由でデータベースファイルを共有
- 同時読み取り用に
DatabasePool(notDatabaseQueue) を使用 - ウィジェットで読み取り専用接続を開く:
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+、axiom-visionOS 2+
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- megastep
- ライセンス
- MIT
- 最終更新
- 2026/3/10
Source: https://github.com/megastep/codex-skills / ライセンス: MIT
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。