← すべての記事

Live Activitiesはバッジではなくステートマシンである

ReturnのLive Activityは、ロック画面とDynamic Islandに表示されるカウントダウン数字のように見えます。12 しかし、これは数字ではありません。3つの外部解除パスと、自分自身から自分を守らなければならない1つの再入可能なスタートパスを持つ、5つのライフサイクル状態を備えたステートマシンです。以下のパターンは、本番環境を生き延びたものです。最後にある「容赦ない正直さ」のフッターには、まだ把握できていないことを記しています。

私がリリースしたv1では、Live Activityをバッジとして扱っていました。「現在の残り時間」がデータで、それ以外は装飾でした。そのバージョンには、TestFlightで発見した3つのバグと、本番環境で発見した1つのバグがありました。

  1. スタート処理が進行中のときにスタートをタップすると、2つ目のActivityが作成され、1つ目が孤立しました。
  2. カウントダウンはDynamic Island上では正しく表示されましたが、ロック画面ビューでは一時停止中のタイマーで endTime <= Date() となり、ユーザーが再開するまで 0:00 と表示されていました。
  3. 解除ポリシーが .default だったため、ユーザーがタイマーをリセットした後も長時間Live Activityが表示されたままでした。Appleはこれを最大4時間表示し続けることがあります。
  4. (本番環境にて。)右から左へ書く言語ロケール(アラビア語、ヘブライ語)では、Dynamic Islandのcompact-trailingリージョンで数字が逆向きにレンダリングされていました。ラテン数字なのにRTLレイアウト。修正は1行で済みました。

これらはすべてステートマシンのバグでした。カウントダウンの数字自体は正しかったのです。数字はプロダクトではありません。プロダクトは状態なのです。

以下のステートマシンは、これらのバグを乗り越えて生き残ったものです。

TL;DR

  • 本番リリース版の LiveActivityManager は、5つの遷移メソッド(startActivityupdateActivityshowCycleCompleteshowFinalCompletionendActivity)と1つの読み取り(hasActiveActivity)を公開しています。224行の本番コードは、startActivity 内の特定のハザード、つまり同時並行的なstart呼び出しと、そのメソッド内の各 await 境界をまたぐキャンセルチェックを防御しています。3
  • ContentState は6つのフィールドを持ちます。endTimecurrentCycletotalCyclesisPausedisCompletedremainingSeconds です。最初の5つはステートマシンのラベルです。6つ目(remainingSeconds)は、ActivityKitのライブ timerInterval では対応できない静的表示用のフォールバックです。
  • 解除ポリシーの判断こそが本当のプロダクト判断です。ユーザーリセット時は .immediate、完了時は .after(Date().addingTimeInterval(3))、システムデフォルトは絶対に使ってはいけません。
  • Dynamic Islandのcompact-trailingリージョンでは、RTLシステムロケール下でもラテン数字をLTRに保つために、タイマーテキストに .environment(\.layoutDirection, .leftToRight) が必要です。

ステートマシン

リリース版のLive Activityには、1つのアイドル状態、ユーザーが観測できる3つのライブ状態、1つの終端状態、そして開発者が観測しなければならない1つの再入可能ゲートがあります。

┌──────────────────────────────────────────────────────────────────┐
│                  Lifecycle states                                 │
├──────────────────────────────────────────────────────────────────┤
│  IDLE          currentActivity == nil; no Live Activity present   │
│  RUNNING       isPaused=false, endTime > Date()                   │
│  PAUSED        isPaused=true, remainingSeconds=N                  │
│  CYCLE_END     isPaused=false, endTime <= Date(), isCompleted=false│
│  COMPLETE      isCompleted=true (terminal; transitions to IDLE)   │
└──────────────────────────────────────────────────────────────────┘
              │
              ↓
┌──────────────────────────────────────────────────────────────────┐
│             Dismissal policies (Apple)                            │
├──────────────────────────────────────────────────────────────────┤
│  .immediate            user reset                                  │
│  .after(now + 3s)      completion display window                   │
│  .default              system decides; can stay up to 4 hours      │
└──────────────────────────────────────────────────────────────────┘

Reentrancy gate inside startActivity():
  isStartingActivity flag + cancellable startActivityTask
  prevents two concurrent startActivity() calls from creating
  two Live Activities for one timer. Cancellation checks across
  each await keep the in-flight task safe to abort.

レンダリングパスでは isPaused を最初にチェックします。この順序こそが、壁時計の時間が endTime を越えても、一時停止中のタイマーが CYCLE_END としてレンダリングされないようにする鍵です。7

状態名は数字に付けるラベルではありません。状態名は、LiveActivityManager(私のSwiftUIビューが生きるアプリ側)と ReturnLiveActivity(Appleのプロセスがサーフェスをレンダリングするウィジェット拡張)の間の契約です。

その契約が TimerActivityAttributes.ContentState で、6つのフィールド全てです。3

public struct ContentState: Codable, Hashable {
    var endTime: Date
    var currentCycle: Int
    var totalCycles: Int?
    var isPaused: Bool
    var isCompleted: Bool = false
    var remainingSeconds: Int = 0
}

すべての状態遷移はこの構造体を変更し、ActivityKitに依頼してプロセス境界を越えてウィジェット拡張に届けます。ウィジェットはそれを受けて再レンダリングします。共有メモリはありません。コールバックもありません。あるのは、すべての遷移でプロセス境界を越えるCodableな構造体だけです。

この事実によって、クロージャ、ビューモデル、observable object、計算プロパティなど、私がやりたいと思うかもしれないあらゆることが排除されます。状態はシリアライズ可能なデータとして表現できなければなりません。エンコードできないものは、遷移できないのです。

再入可能なスタート

Live Activitiesには同時アクティビティ数のハードリミットと、Activity.request を進行中に2回呼んだときに何が起きるかというソフトリミットがあります。ハードリミットは十分にドキュメント化されています。4 ソフトリミットは「2回目の呼び出しが成功して孤立Activityを作成する可能性がある」というものです。孤立Activityとは、マネージャー内の currentActivity と関連付けが切れたLive Activityのことです。それは生き残ります。コードからの戻り経路はありません。最終的には自身の陳腐化タイマーで自然に解除されます。それまでユーザーには重複したタイマーが見えます。

孤立はReturnのv1で出荷したバグでした。修正は再入可能ゲートと、LiveActivityManager.swift 内のキャンセル可能なTaskです。3

private var isStartingActivity = false
private var startActivityTask: Task<Void, Never>?

func startActivity(...) {
    #if os(iOS)
    guard ActivityAuthorizationInfo().areActivitiesEnabled else { return }
    guard !isStartingActivity else { return }
    isStartingActivity = true

    startActivityTask?.cancel()

    startActivityTask = Task {
        defer {
            isStartingActivity = false
            startActivityTask = nil
        }
        guard !Task.isCancelled else { return }

        await endActivity()  // explicit cleanup of any prior state

        guard !Task.isCancelled else { return }

        // ... build attributes + contentState ...

        do {
            let activity = try Activity.request(...)
            guard !Task.isCancelled else { return }
            currentActivity = activity
        } catch {
            // log; flag clears via defer
        }
    }
    #endif
}

このパターンについて、ドキュメントが触れていない3つのことがあります。

isStartingActivity フラグが能動的な保護で、startActivityTask?.cancel() は防御的なクリーンアップです。 フラグは1つ目のスタート処理が進行中の間、2つ目の startActivity 呼び出しを短絡させるので、実際にはパブリックパスで競合することはありません。それでもキャンセル後に置き換えるダンスが重要なのは、進行中のTaskが非同期で短命な呼び出し元より長く生きられるからです。キャンセルによって、呼び出し元が次に進んだ後も古いTaskが動き続けるのを防ぎます。

guard !Task.isCancelled チェックは各 await 境界をまたぎます。 Swiftにおけるキャンセルは協調的です。cancelが呼ばれても、Taskは明示的にチェックするまで動き続けます。各 await がチェックの機会となります。await後のチェックがなければ、キャンセルされたTaskはActivity状態を構築し続け、Activity.request を呼び出し、成功時に静かに孤立Activityを作成してしまいます。

defer がTask本体の完了前にフラグをクリアします。 defer がなければ、(キャンセルチェックからの)早期 return によって isStartingActivity = true が永続的に残り、アプリを再起動するまでActivityが二度と開始されなくなります。フラグはロックです。ロックはすべての終了パスで解放されなければなりません。

pushType: nil 引数について。 ReturnはAPNsプッシュによるLive Activity更新を使用していません。アプリは activity.update を介してローカルでActivityを更新します。プッシュ駆動の更新(配送トラッキング、スポーツのスコア、リアルタイムデータ)が必要なら、型は pushType: .token になり、契約は劇的に複雑になります。5 ローカル更新の方がシンプルで、タイマー/カウンター/単一アプリのワークフローならカバーできます。

一時停止の問題

ActivityKitには Text(timerInterval: Date()...endTime, countsDown: true) という美しいビューがあり、アプリからの更新なしでライブカウントダウンをレンダリングします。6 終了時刻を設定すれば、システムがライブタイマーをレンダリングします。Timer.publish も、ウィジェット更新も、バッテリー消費もありません。

タイマーが動いているときには素晴らしい機能です。一時停止しているときには間違った動作になります。

timerInterval のテキストは、状態の中の「pause」シグナルに関係なく endTime に向かってカウントしていきます。AppleのAPIには「10:23で凍結」モードはありません。endTime = Date().addingTimeInterval(623) を渡し、ユーザーが10:23の時点で一時停止すると、ウィジェット内のタイマーテキストはゼロまでカウントダウンを続けます。状態フィールドはpausedを示しています。ウィジェットはrunningをレンダリングしてしまうのです。

修正は、同じ状態から2つの異なるビューをレンダリングすることです。7

if context.state.isPaused {
    // static text
    Text(formatTime(context.state.remainingSeconds))
        .monospacedDigit()
} else if context.state.endTime > Date() {
    // live countdown
    Text(timerInterval: Date()...context.state.endTime, countsDown: true)
        .monospacedDigit()
} else {
    // post-end static
    Text("0:00")
        .monospacedDigit()
}

この2系統のレンダリングこそ、ContentStateremainingSeconds を別フィールドとして持つ理由です。タイマーが動いているときは冗長です(システムが endTime から計算します)。一時停止中のときは唯一の真実の源になります。構造体の2つの半分は、2つの異なるレンダリングモードに対応しており、isPaused のbooleanがそれらを切り替えるのです。

解除ポリシー

activity.end(_:dismissalPolicy:) は3つの ActivityUIDismissalPolicy 値のうち1つを取ります。誤った選択をしたせいで、私のv1ではリセット後にユーザーのロック画面に永遠にも感じられるほど長く表示が残ってしまいました。13

ポリシー 使うとき 得られる動作
.immediate ユーザーリセット、エラー、追跡対象なしでアプリがバックグラウンド化 Activityが即座に消える。猶予期間なし
.after(date) 完了表示。「瞑想が完了しました」を一瞬読めるようにしたい場合。日付はApple が許容する4時間の枠内である必要があります Activityが最終状態を表示し、その後 date で解除
.default Appleのヒューリスティクスに本気で判断を委ねたいとき システムが「しばらくの間」可視のまま保持(Appleの表現)、end 呼び出しから最大4時間

Returnは自然な完了パスでは .after(Date().addingTimeInterval(3)) を使用しています。3

await activity.end(
    .init(state: contentState, staleDate: nil),
    dismissalPolicy: .after(Date().addingTimeInterval(3))
)

3秒は、ユーザーがロック画面を一目見て、タイマーが終了したと認識し、チェックマークの満足感を味わうのに必要な時間です。3秒未満だとせわしないですし、3秒を超えるとActivityが終了を認識していないように感じられます。

ユーザートリガーのリセットの場合、呼び出しは dismissalPolicy: .immediate になります。猶予なし。ユーザーはすでに知っているのですから。

v1での誤った選択は .default でした。完了した瞑想タイマーに対してシステムがActivityを長時間表示し続けたため、ユーザーはアプリが完了を認識していないと考えました。Appleのドキュメントでは .default は終了したActivityを「しばらくの間」最大4時間まで可視に保つと述べています。13 タイマーに対する正しい姿勢は、解除を明示的に行うことです。

Dynamic Islandのcompactリージョン

Dynamic Islandには3つのレンダリングモードがあり、シンプルなタイマーであっても3つすべてが必要になります。2

  • Compact(デフォルトのDynamic Island形状):先頭にアイコン+末尾にタイマー
  • Minimal(別のLive Activityが同じDynamic Islandを取り合うとき):先頭のアイコンのみ
  • Expanded(長押し時):4つの名前付きリージョン(leadingtrailingcenterbottom

Returnで採用したパターンは、expandedビューをcompactとほぼ同一にすることです。8

DynamicIsland {
    DynamicIslandExpandedRegion(.leading) {
        Image("AppIconSmall")
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: 16, height: 16)
            .clipShape(RoundedRectangle(cornerRadius: 4))
    }
    DynamicIslandExpandedRegion(.trailing) {
        TimerText(...)
    }
    DynamicIslandExpandedRegion(.center) { EmptyView() }
    DynamicIslandExpandedRegion(.bottom) { EmptyView() }
} compactLeading: {
    Image("AppIconSmall")...
} compactTrailing: {
    TimerText(...)
} minimal: {
    Image("AppIconSmall")...
}

ほとんどのLive Activityチュートリアルは、bottom リージョンに豊かなコンテンツを置いた expanded ビューを「本物の」デザインとして強調します。瞑想タイマーにとっては、expansion は無駄な重荷です。ユーザーは長押しでexpandedビューを開きますが、長押し自体ですでに何かが起きたという触覚フィードバックを得ています。コンテンツを足すと、ユーザーが求めていないことを expansion が語ってしまうことになります。expandedモードでの空のリージョンはデザインの失敗ではなく、デザインそのものなのです。

RTLのバグ

本番環境のバグです。iOSでアラビア語とヘブライ語のユーザーから、Dynamic Islandのcompact-trailingタイマーで数字が逆向きにレンダリングされるという報告がありました。ラテン数字の文字列 5:2332:5 としてレンダリングされていたのは、compact-trailingのレイアウト方向がシステムロケールのRTL設定を継承していたからです。

SwiftUIはウィジェットプロセス内でシステムレイアウト方向を継承するため、ユーザーの携帯がアラビア語またはヘブライ語に設定されていると、Dynamic IslandのタイマーテキストはRTLを取り込みます。ラテン数字は、たとえRTLのUIの中であってもLTRでレンダリングされるべきです。修正は、数字テキストビューにレイアウト方向を固定することです。7

.environment(\.layoutDirection, .leftToRight)

このオーバーライドは、TimerText 内(Dynamic Islandのcompact/expanded)とロック画面ビュー内の数値の Text ビューに対して適用し、ビュー全体には適用しません。ラテン数字はユーザーのシステムロケールに関わらず左から右に読まれます。一方で「Cycle 2 of 3」のようなサイクルラベルはローカライズされたままにし、システムレイアウト方向に従わせます。

このバグは国内ロケールのTestFlightでは表面化しません。実際のRTLユーザーがタイマーを開いた瞬間に表面化します。教訓は、RTLロケールで動作する可能性のあるすべてのLive Activityにおいて、ラテン数字を含むすべてのテキストビューにLTR固定の environment オーバーライドを出荷することです。

ローカライゼーションの話

TimerActivityAttributes は、Activity作成時にアプリが設定する languageCode: String フィールドを持ちます。9

let attributes = TimerActivityAttributes(
    timerDuration: duration,
    languageCode: settings.appLanguage  // app's selected language, not system's
)

ウィジェット拡張はこれを読んでローカライズされた文字列をレンダリングします。

private var locale: Locale {
    let code = context.attributes.languageCode
    return code.isEmpty ? .current : Locale(identifier: code)
}

private func localized(_ key: String.LocalizationValue) -> String {
    String(localized: key, locale: locale)
}

ウィジェットに Locale.current を読ませるのではなく、アプリが自身の言語コードを渡す理由は、ウィジェット拡張が独自プロセスで動作するからです。その Locale.current はシステムロケールであり、アプリで選択されたロケールではありません。ユーザーがiPhoneを英語にしたままReturnを韓国語に設定していた場合、このオーバーライドがなければウィジェットは英語で話してしまいます。アプリの言語設定はActivity属性内を旅し、ウィジェットはそれを尊重します。

Localizable.xcstrings はアプリと並んでウィジェットターゲットにも存在しますが、別々のファイルです。ウィジェットで使用される文字列は、たとえ同じ文字列が Return/Localizable.xcstrings に存在していても、ReturnWidgets/Localizable.xcstrings にも存在しなければなりません。これを忘れると、アプリは韓国語で話しているのに、ウィジェットは開発言語にフォールバックしてしまいます。

もう一度作るならこうする

ContentState を小さくする。 6フィールドは多すぎます。endTimeremainingSeconds の冗長性は、timerInterval に一時停止モードがないことを回避するための代償です。最初からやり直すなら、単一の displayMode 列挙型(runningpaused(remainingSeconds: Int)cycleEndcomplete)を持たせ、ケースごとにレンダリングコードをディスパッチさせるでしょう。6つのフィールドを5つの遷移メソッドにまたがって正しく変更し続けるよりも、4つのケースの方が容易です。

インタラクティブなLive Activityボタンを追加する(iOS 17+)。 Returnは現時点ではDynamic Islandに一時停止/再開コントロールを公開していません。ユーザーは一時停止するためにアプリを開かなければなりません。iOS 17ではLive Activity内のApp Intentsに対して Button(intent:) が追加されました。10 インタラクティブな一時停止コントロールは明らかな拡張であり、Returnで次にリリースする予定のものです。

クロスデバイスのタイマー同期のためのプッシュ更新Live Activities。 Returnは NSUbiquitousKeyValueStore を介してiPhone、iPad、Watch、Apple TV間でセッションを同期しています(5つのAppleプラットフォーム、3つの共有ファイルで取り上げました)。今日、ActivityはiPhoneまたはiPadアプリからローカルで開始され、ローカルで更新されます。Apple Watchでタイマーを開始したユーザーが、それがiPhoneのLive Activityにリアルタイムで反映されるのを理想的には見られるはずです。Live ActivityへのAPNsプッシュがその経路です。5 まだ構築していません。

Live Activitiesを使うべきでないとき

1回限りの一過性の状態。 「保存しました!」というトーストにLive Activityはふさわしくありません。システムにバナーがあります。それを使ってください。

タイマー次元のない頻繁に変わるデータ。 Live Activitiesは、明確な時間的アンカー(タイマー、配送ETA、ゲームの時計、通話の継続時間)を持つものに最も適しています。株価ティッカーやスポーツのスコアが機能するのは、セッションウィンドウがあるからです。汎用ダッシュボードは違います。

ロック画面/スタンバイのユースケースがないアプリ。 Live Activitiesは実際のエンジニアリング投資(ターゲットセットアップ、ContentState設計、解除ポリシーの判断、RTL対応、ローカライゼーション配管)を必要とします。使用中にユーザーがロック画面を一切参照することなく直接開くアプリは、適切な形ではありません。写真エディタには必要ありません。ワークアウトトラッカーには必要です。

iOS以外のサーフェスでは注意が必要。 Returnの LiveActivityManager は、タイマーがiPhoneまたはiPadアプリから開始されるため、#if os(iOS) の背後に実装を出荷しています。ActivityKit自体は、ロック画面バナー、Dynamic Island、Apple WatchのSmart Stack、Mac、CarPlayをプレゼンテーションサーフェスとして説明しており、iOS 26はそのうちのいくつかを拡張しました。4 watchOSには依然としてフルスクリーンレンダリング用の独自コンプリケーションAPIがあります。macOSにはメニューバーアプリがあります。iPadOSはiPadOS 17以降、Dynamic Islandリージョンなしでもライブアクティビティをサポートしています。Returnのマネージャーは、224行の1ファイル全体に8つの #if os(iOS) ガードを持ちます。

このパターンがiOS 26+でリリースするアプリに意味すること

2つのポイントです。

  1. Live Activityを数字ではなくステートマシンとして扱う。 ステートマシンには明確な状態、明確な遷移、明確な解除ルールがあります。画面上の数字は1つの状態の1つのレンダリングに過ぎません。まず状態を正しく決めましょう。

  2. 再入ガードは、まだ遭遇していないバグです。 私が現場で見てきた isStartingActivity +キャンセル可能なTaskを実装していないLive Activityマネージャーはすべて、少なくとも1つの孤立Activityのバグを出荷しています。ガードは6行です。一度書いてください。

この投稿は、同じファミリーのアプリに対する以前の記事と組み合わせて読んでください。Apple Intelligence向けの型付きApp Intents、クロスLLMエージェント向けのMCPサーバー、ビジュアルレイヤー向けのLiquid Glassパターン、クロスデバイスリーチ向けのマルチプラットフォーム配信。Live Activitiesは、同じスタックのiOSロック画面とDynamic Islandのレイヤーです。完全なセットはApple Ecosystem Series ハブにあります。より広範なiOS×AIエージェントの文脈については、iOS Agent Developmentガイドを参照してください。

FAQ

Live ActivitiesとWidgetKitウィジェットの違いは何ですか?

WidgetKitウィジェットは TimelineProvider で定義された間隔でレンダリングされ、システムが更新タイミングを判断し、ウィジェットは静的なタイムラインから再レンダリングします。11 Live Activitiesは、特定のアプリ駆動の activity.update(...) 呼び出しに応じてレンダリングされ、基礎となるアクティビティ(タイマー、配送、ワークアウト)の継続時間にわたって生存します。両方ともウィジェット拡張ターゲットでリリースされます。違いはトリガーモデルです。

Live ActivitiesはiPadで動作しますか?

はい、iPadOS 17以降で動作します。ロック画面バナーが主要なレンダリングサーフェスです。iPadにはDynamic Islandがありません。同じ ActivityConfiguration コードが動作します。Dynamic IslandリージョンがiPadでは決してレンダリングされないことだけは想定しておきましょう。

Live Activityは私のアプリプロセスより長く生きられますか?

はい。Activity.request が成功すると、ActivityKitがそのActivityを所有します。アプリプロセスはシステムによって終了させられる可能性があります。Activityは、明示的に終了するか(あるいはシステムの陳腐化ルールが解除するまで)、ロック画面とDynamic Islandでレンダリングを続けます。明示的な endActivity() 呼び出しが重要なのはそのためです。アプリリセット時に明示的な終了がなければ、Activityはタイマーより長く生き残ります。

なぜこの投稿はプッシュ更新Live Activitiesを取り上げないのですか?

私はReturnでプッシュ更新Live Activitiesを出荷していません。このクラスタのジャンルルールに従い、出荷済みコードの投稿は本番コードが実際に行うことだけを文書化します。プッシュ更新は「もう一度作るならこうする」に挙げられています。出荷した後に、それを取り上げる将来の投稿が登場するでしょう。

SwiftUIアプリにおけるLive Activitiesの実際のファイル構成は?

3つの部分です。3712

  • メインアプリターゲット内: LiveActivityManager.swift(Activityのライフサイクルを管理)、TimerActivityAttributes.swift(ウィジェットと共有する ActivityAttributes 構造体。両方のターゲットがこのファイルをコンパイルします)。
  • ウィジェット拡張ターゲット内: ReturnLiveActivity.swiftActivityConfiguration 本体を持つ Widget 適合)、ReturnWidgetsBundle.swift@main WidgetBundle)。
  • 設定: アプリターゲットの Info.plistNSSupportsLiveActivities = YES

ウィジェット拡張ターゲットにはActivityKitとWidgetKitのインポートが必要です。TimerActivityAttributes は両方のターゲット間で共有される唯一のファイルです。それ以外はすべてターゲット分離されます。


Live Activityはロック画面の数字ではありません。すべての遷移でプロセス境界を越えるステートマシンです。状態を正しく決め、再入を守り、解除ポリシーを意図的に選び、レイアウト方向を固定する。数字はそれだけで自ずと整います。

References


  1. 著者のReturn。SwiftUI製の瞑想タイマーで、2026年4月21日にApp Storeで公開され、iPhone、iPad、Mac、Apple Watch、Apple TVで利用可能。Live ActivitiesはiOSターゲットでのみリリース。 

  2. Apple Developer、“ActivityKit framework”。ロック画面バナー、Dynamic Islandのcompact/minimal/expandedモード、Activityのライフサイクル。iOS 16.1以降で利用可能。Dynamic IslandはiPhone 14 Pro以降で利用可能。 

  3. Return/Return/LiveActivityManager.swift(224行、#if os(iOS) ブロックが8つ)と Return/Return/TimerActivityAttributes.swift(43行)の本番コード。アプリターゲットとウィジェット拡張ターゲット間でターゲットメンバーシップを介して共有。 

  4. Apple Developer、“Displaying live data with Live Activities”。同時実行制限、サポートプラットフォーム(iOS 16.1以降、iPadOS 17以降)、NSSupportsLiveActivities のInfo.plistキー。 

  5. Apple Developer、“Updating and ending your Live Activity with ActivityKit push notifications”pushType: .token パスには、別のAPNs認証キー、サーバーサイドのプッシュトークン登録、ローカルの activity.update(...) 呼び出しとは異なる更新プロトコルが必要。 

  6. Apple Developer、“Text(timerInterval:pauseTime:countsDown:showsHours:)”。システムレンダリングのライブカウントダウンタイマー。Activityが動作中はアプリの更新なしでレンダリングされる。 

  7. Return/ReturnWidgets/ReturnLiveActivity.swift(232行)の本番コード。ウィジェット拡張の ActivityConfiguration<TimerActivityAttributes> 本体を持つ Widget 適合。61〜102行目の TimerText ビューが、一時停止/実行中/終了後の3状態レンダリングを処理。 

  8. Apple Developer、“DynamicIsland”。4つの名前付き expanded リージョン(leadingtrailingcenterbottom)と3つのcompactモードビュー(compactLeadingcompactTrailingminimal)。 

  9. ウィジェット拡張は独自のプロセスで動作し、アプリの選択ロケールではなくシステムロケールを継承する。アプリ内言語切り替えをサポートするアプリ(Returnは27言語をサポート)は、ActivityAttributes を介して言語コードを渡し、ウィジェットがユーザーの選択した言語でレンダリングできるようにしなければならない。パターン:Locale.current ではなく Locale(identifier: context.attributes.languageCode)。 

  10. Apple Developer、“Button(intent:)”。iOS 17以降のウィジェットおよびLive Activityビューで利用可能。アプリのフォアグラウンド化を要求することなくApp Intentsをロック画面/Dynamic Islandコントロールに橋渡しする。 

  11. Apple Developer、“TimelineProvider”。Live Activitiesに先行するウィジェット更新モデル。事前計算されたエントリとシステム管理のリロードウィンドウ。 

  12. Return/ReturnWidgets/ReturnWidgetsBundle.swift(16行)の本番コード。ウィジェット拡張の唯一のウィジェットとして ReturnLiveActivity を登録する @main WidgetBundle。ウィジェット拡張に必要なパターン。バンドルがシステムにロードされる対象。 

  13. Apple Developer、“ActivityUIDismissalPolicy”。3つのケース:.default.immediate.after(_:)。Appleは .default が終了したLive Activityを「しばらくの間」最大4時間まで可視に保つと述べており、.after(_:) は同じ4時間枠内の日付を受け入れる。 

関連記事

iOS 26のウィジェット面:1つのApp Intentが、いくつもの場所で動く

iOS 26のウィジェット、コントロールセンターのコントロール、そしてLive Activity——これらはすべてApp Intentの表示面です。1つのintentが、ボタンも、コントロールも、Live Activityのアクションも動か…

2 分で読める

iOS 26のHealthKit + SwiftUI:認可、サンプルタイプ、そして2つの実アプリから得たクロスプラットフォームパターン

Water(水分摂取トラッキング、HKQuantitySample)とReturn(マインドフルセッション、HKCategorySample)から得た本番運用パターン。許可UX、async ラッパー、watchOSバリアント、そして避けるべ…

4 分で読める

ループ・エンジニアリング――検証が安いところでループは勝つ

ループ・エンジニアリングを、Boris Cherny の全文書き起こしと照合する。彼が名前を挙げるループはどれも検証が安い。その制約こそが、何を自動化すべきかを決める。

4 分で読める