CLAUDE LABEN
WWDC — WWDC 2026でSiriはGoogle Geminiベースと確定。ChatGPT等への外部ハンドオフは廃止され、サードパーティAI選択はEU(DMA)で当面非対応にBILLING — 6/15の課金変更まで残り6日。Agent SDK・headless Claude Code・GitHub Actions・他社エージェントがAPIレート準拠の月次クレジットへ移行OUTAGE — claude.ai・Claude Code・Coworkで障害が報告(6月)。スケジュール実行はfallbackModelとリトライ前提の設計が安全ですDYNAMIC-WORKFLOWS — Max・TeamプランとAPIでdynamic workflowsがデフォルトON。コードベース横断のバグ探索や独立検証に活用ULTRACODE — Claude Codeの新設定ultracodeがeffortメニューに追加。xhigh固定でワークフロー判断はClaudeに委ねますOPUS4.8 — Claude Opus 4.8が主要プランのデフォルトとして定着。コーディング・エージェント・推論を強化WWDC — WWDC 2026でSiriはGoogle Geminiベースと確定。ChatGPT等への外部ハンドオフは廃止され、サードパーティAI選択はEU(DMA)で当面非対応にBILLING — 6/15の課金変更まで残り6日。Agent SDK・headless Claude Code・GitHub Actions・他社エージェントがAPIレート準拠の月次クレジットへ移行OUTAGE — claude.ai・Claude Code・Coworkで障害が報告(6月)。スケジュール実行はfallbackModelとリトライ前提の設計が安全ですDYNAMIC-WORKFLOWS — Max・TeamプランとAPIでdynamic workflowsがデフォルトON。コードベース横断のバグ探索や独立検証に活用ULTRACODE — Claude Codeの新設定ultracodeがeffortメニューに追加。xhigh固定でワークフロー判断はClaudeに委ねますOPUS4.8 — Claude Opus 4.8が主要プランのデフォルトとして定着。コーディング・エージェント・推論を強化
記事一覧/Claude Code
Claude Code/2026-04-18上級

Claude Code で作るiOS 18 ウィジェット & App Intents 実装ガイド

WidgetKit と App Intents の実装をClaude Codeで加速する。インタラクティブウィジェット・Live Activities・Siri連携まで、動くコードと落とし穴を網羅した実践ガイド。

Claude Code219iOS 18WidgetKitApp IntentsSwift4SwiftUI7Live Activities

プレミアム記事

iOS 18 のウィジェット開発で最初にぶつかる壁は、「なぜ更新されないのか」という問題です。タイムラインを設定しても、アプリを更新してもホーム画面の表示が変わりません。WidgetKit のデバッグはシミュレーターだけでは再現しにくく、実機のログを追いながら原因を絞り込む作業が続きます。

Claude Code をこのデバッグプロセスに組み込んでから、私の開発サイクルは大きく変わりました。エラーログをそのまま貼り付けて「このタイムライン設計の何が問題か」と問えば、WidgetKit 固有の制約まで踏まえた回答が返ってきます。実際に Claude Code を使いながら iOS 18 のウィジェット機能を実装して気づいた知見を、動くコードとともに記録しておきます。

WidgetKit の基礎と iOS 18 での変化

タイムラインとエントリの設計

WidgetKit の中核は TimelineProvider です。iOS 18 からインタラクティブウィジェットが正式対応となり、ボタンタップやトグル操作がウィジェット上で完結できるようになりました。

まず基本的な構造を確認します。

import WidgetKit
import SwiftUI
 
// タイムラインエントリ(表示データの単位)
struct TaskEntry: TimelineEntry {
    let date: Date
    let taskCount: Int
    let nextDeadline: Date?
    let completionRate: Double
}
 
// タイムラインプロバイダー
struct TaskTimelineProvider: TimelineProvider {
    typealias Entry = TaskEntry
    
    // ウィジェット追加直後のプレースホルダー
    func placeholder(in context: Context) -> TaskEntry {
        TaskEntry(
            date: Date(),
            taskCount: 3,
            nextDeadline: Calendar.current.date(byAdding: .hour, value: 2, to: Date()),
            completionRate: 0.6
        )
    }
    
    // ウィジェットギャラリーのプレビュー
    func getSnapshot(in context: Context, completion: @escaping (TaskEntry) -> Void) {
        // スナップショットはできるだけ速く返す(非同期処理は最小限に)
        let entry = TaskEntry(
            date: Date(),
            taskCount: 5,
            nextDeadline: Calendar.current.date(byAdding: .hour, value: 1, to: Date()),
            completionRate: 0.8
        )
        completion(entry)
    }
    
    // 実際のタイムライン生成
    func getTimeline(in context: Context, completion: @escaping (Timeline<TaskEntry>) -> Void) {
        var entries: [TaskEntry] = []
        let now = Date()
        
        // App Group 経由でデータを取得
        // ⚠️ 直接 UserDefaults.standard を使うとウィジェットからアクセスできない
        let sharedDefaults = UserDefaults(suiteName: "group.com.yourapp.shared")
        let taskCount = sharedDefaults?.integer(forKey: "taskCount") ?? 0
        let completionRate = sharedDefaults?.double(forKey: "completionRate") ?? 0.0
        
        // 今後1時間分のエントリを15分刻みで生成
        for minuteOffset in stride(from: 0, to: 60, by: 15) {
            let entryDate = Calendar.current.date(
                byAdding: .minute,
                value: minuteOffset,
                to: now
            )\!
            entries.append(TaskEntry(
                date: entryDate,
                taskCount: taskCount,
                nextDeadline: sharedDefaults?.object(forKey: "nextDeadline") as? Date,
                completionRate: completionRate
            ))
        }
        
        // .atEnd: 最後のエントリ後にシステムが再度 getTimeline を呼ぶ
        // .after(date): 指定時刻に再取得
        // .never: 手動更新のみ(WidgetCenter.shared.reloadTimelines で)
        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

Claude Code にこのコードを渡して「タイムラインが更新されない場合のデバッグチェックリストを作って」と依頼すると、App Group の設定漏れ、Background refresh の権限、タイムライン更新頻度の制限(1時間あたり最大40〜70回)といった見落としがちなポイントを体系的に提示してくれます。

インタラクティブウィジェットの実装(iOS 17+)

iOS 17 から導入されたインタラクティブウィジェットは、ButtonToggle をウィジェット内で直接使えます。ポイントは AppIntent との連携です。

import AppIntents
import WidgetKit
 
// ウィジェットから呼ばれる AppIntent
struct CompleteTaskIntent: AppIntent {
    static var title: LocalizedStringResource = "タスクを完了"
    static var description = IntentDescription("ウィジェットからタスクを完了します")
    
    // パラメーターで対象タスクIDを受け取る
    @Parameter(title: "タスクID")
    var taskId: String
    
    func perform() async throws -> some IntentResult {
        // App Group 経由でタスクを更新
        let sharedDefaults = UserDefaults(suiteName: "group.com.yourapp.shared")
        var completedIds = sharedDefaults?.stringArray(forKey: "completedTaskIds") ?? []
        
        guard \!completedIds.contains(taskId) else {
            return .result()
        }
        
        completedIds.append(taskId)
        sharedDefaults?.set(completedIds, forKey: "completedTaskIds")
        
        // ウィジェットのタイムラインをリロード
        // ⚠️ perform() 内では WidgetCenter を呼んでも即時反映されない場合がある
        // → Background Task として登録するか、アプリ起動後に reloadAll を呼ぶ
        WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget")
        
        return .result()
    }
}
 
// ウィジェットのビュー(インタラクティブ対応)
struct TaskWidgetView: View {
    let entry: TaskEntry
    
    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text("今日のタスク")
                .font(.caption)
                .foregroundStyle(.secondary)
            
            Text("\(entry.taskCount)件")
                .font(.title2.bold())
            
            // インタラクティブボタン(iOS 17+)
            // Button のクロージャー内では AppIntent を指定する
            Button(intent: CompleteTaskIntent(taskId: "sample-id")) {
                Label("完了", systemImage: "checkmark.circle")
                    .font(.caption)
            }
            .buttonStyle(.borderedProminent)
            .tint(.green)
            
            // 進捗バー
            ProgressView(value: entry.completionRate)
                .tint(entry.completionRate > 0.8 ? .green : .orange)
        }
        .padding()
        .containerBackground(.fill.tertiary, for: .widget)
    }
}

Claude Code へのプロンプト例: 「このインタラクティブウィジェットで、ボタンタップ後に即座にUIが変わらない場合の原因と対処法を教えて」

返ってくる回答では、perform() の完了からウィジェット再描画までに1〜2秒のラグがあること、WidgetCenter.shared.reloadTimelines は非同期で確実性がないこと、Optimistic UI パターン(先に表示を変えてから確認する)の実装方法が具体的に示されます。

Live Activities の設計と実装

ActivityKit の基本構造

Live Activities は Dynamic Island とロック画面に表示されるリアルタイム更新 UI です。配達状況、スポーツスコア、長時間タスクの進捗などに最適です。

import ActivityKit
import SwiftUI
 
// Live Activity の属性定義
struct BuildTaskAttributes: ActivityAttributes {
    // 変更されない静的データ
    public struct ContentState: Codable, Hashable {
        var progress: Double        // 0.0 〜 1.0
        var currentStep: String
        var elapsedSeconds: Int
        var isComplete: Bool
    }
    
    let taskName: String            // 起動時に決まる静的情報
    let startedAt: Date
}
 
// Live Activity の開始
func startLiveActivity(taskName: String) -> Activity<BuildTaskAttributes>? {
    guard ActivityAuthorizationInfo().areActivitiesEnabled else {
        print("Live Activities が無効です(設定 → 通知 → ライブアクティビティ を確認)")
        return nil
    }
    
    let attributes = BuildTaskAttributes(
        taskName: taskName,
        startedAt: Date()
    )
    
    let initialState = BuildTaskAttributes.ContentState(
        progress: 0.0,
        currentStep: "準備中",
        elapsedSeconds: 0,
        isComplete: false
    )
    
    let content = ActivityContent(
        state: initialState,
        staleDate: Calendar.current.date(byAdding: .minute, value: 30, to: Date())
        // staleDate を過ぎると「情報が古い」表示になる
    )
    
    do {
        let activity = try Activity.request(
            attributes: attributes,
            content: content,
            pushType: .token  // プッシュ通知でリモート更新する場合
        )
        return activity
    } catch {
        // よくあるエラー: com.apple.ActivityKit.ActivityKitError 4
        // → Info.plist に NSSupportsLiveActivities を追加し忘れ
        print("Live Activity 起動失敗: \(error)")
        return nil
    }
}
 
// 進捗の更新
func updateActivity(_ activity: Activity<BuildTaskAttributes>, step: String, progress: Double) async {
    let updatedState = BuildTaskAttributes.ContentState(
        progress: progress,
        currentStep: step,
        elapsedSeconds: Int(Date().timeIntervalSince(activity.attributes.startedAt)),
        isComplete: progress >= 1.0
    )
    
    await activity.update(
        ActivityContent(
            state: updatedState,
            staleDate: Calendar.current.date(byAdding: .minute, value: 5, to: Date())
        )
    )
}
 
// Live Activity の UI(Dynamic Island コンパクト表示)
struct BuildActivityCompactView: View {
    let state: BuildTaskAttributes.ContentState
    
    var body: some View {
        HStack {
            Image(systemName: state.isComplete ? "checkmark.circle.fill" : "hammer.fill")
                .foregroundStyle(state.isComplete ? .green : .orange)
            
            ProgressView(value: state.progress)
                .frame(width: 60)
            
            Text(String(format: "%.0f%%", state.progress * 100))
                .font(.caption2.monospacedDigit())
        }
    }
}

Claude Code を使った Live Activities のデバッグ

Live Activities のデバッグで厄介なのは、エラーメッセージが曖昧な点です。ActivityKitError のエラーコードだけ返ってきて、原因が分かりにくいことがあります。

Claude Code に以下のようなプロンプトを送ると、原因の絞り込みが一気に進みます。

以下のエラーで Live Activity が起動しません。
エラー: ActivityKitError(code: 4)
環境:
- iOS 18.3.1
- Xcode 16.2
- Target: iOS 17.0+
- Info.plist に NSSupportsLiveActivities を追加済み

考えられる原因と確認手順を教えてください。

返ってくる回答(私が実際に役立てたもの):

  1. NSSupportsLiveActivities の設定先ミス: メインターゲットではなく Extension のターゲットに追加している場合がある
  2. App Group の不一致: Live Activity と本体アプリで異なる App Group を参照している
  3. シミュレーターでの制限: 一部の Live Activities 機能はシミュレーターで動作しない(実機必須)
  4. プッシュ通知の権限: pushType: .token を指定しているのに APNs の設定が不完全

実際に私のケースでは「2」が原因でした。UserDefaults(suiteName:) の suite 名と、Capabilities で設定した App Group の識別子が1文字異なっていました。このような地味なミスを Claude Code との対話で体系的に潰せるのが大きな時短になります。

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
複数ウィジェットを持つアプリでタイムライン更新バジェットを枯渇させない、コンテンツ別の更新間隔の設計判断
Widget Extension の約30MBメモリ上限を超えないための ImageIO ダウンサンプリング実装(元画像6MB→約2MBの実測付き)
App Group で画像を参照渡しする安全なデータ共有パターンと、Crashlytics・Firebase 運用との接続
Stripe による安全な決済 · いつでもキャンセル可能
シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

Claude Code2026-04-15
Claude Code × SwiftUI × StoreKit 2 — アプリ内課金・サブスクリプション実装ガイド
Claude CodeとSwiftUI・StoreKit 2を組み合わせたアプリ内課金・サブスクリプション実装の実践ガイドです。トランザクション処理・サーバー検証・ペイウォールUIまで、App Store審査を通過する本番品質コードを、個人開発12年の運用知見とともにまとめています。
Claude Code2026-04-12
Claude Code × Swift/iOS 本番アプリ開発 — Xcode連携・Clean Architecture・App Store公開まで
Claude CodeをXcodeと連携させてSwift/iOSアプリを本番品質で開発する完全ガイド。CLAUDE.md設計・SwiftUI実装・Clean Architecture・テスト自動生成・Fastlane CI/CDによるApp Store公開まで体系的に解説します。
Claude Code2026-05-15
Claude Code で SwiftUI アニメーションの迷いをなくす — 5,000万DL壁紙アプリで実践したプロンプト設計
SwiftUI アニメーションの実装でClaude Codeをどう活用するか。Beautiful HD Wallpapers(累計5,000万DL超)の開発現場で実際に使ったプロンプトパターンと、Before/Afterコード例を具体的に紹介します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →