activitykit について
ActivityKitは、ロック画面やDynamic Islandに表示するライブアクティビティの実装ガイドです。iOS開発者向けに、状態やライフサイクル、表示、APNsによる更新とペイロード検証を扱います。
主な機能
- 01静的属性と動的状態の定義
- 02アクティビティの開始・更新・終了
- 03ロック画面とDynamic Islandのレイアウト設計
- 04APNs経由の更新送信
- 05予約開始とチャンネル配信への対応
活用シーン
- 01ロック画面で配送状況を表示
- 02Dynamic Islandで試合スコアを表示
- 03サーバーからアクティビティ状態を更新
- 04開始時刻が決まったイベントの予約
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill activitykitインストール後、Claude Code で/activitykitを入力して読み込まれたか確認します。
同梱ファイルevals/evals.jsonreferences/activitykit-patterns.md
詳細
ActivityKit
ActivityKit は、ロック画面と Dynamic Island に表示される、リアルタイムでひと目で確認できる Live Activities を管理します。通常のタイムラインウィジェットは widgetkit に、汎用 APNs のセットアップは push-notifications に属します。ActivityKit が管理するのは、Live Activity のライフサイクルとペイロードの契約です。aps.content-state は、連携して使用するカスタム日付/範囲のエンコーディングを含め、ActivityAttributes.ContentState と完全に一致する形にデコードできなければなりません。特に記載がない限り、最新の ActivityContent ライフサイクルの例には iOS 16.2 以降が必要です。
プッシュペイロード形式、同時進行するアクティビティ、状態の監視、テストなど、完全なコードパターンについては references/activitykit-patterns.md を参照してください。
目次
- ワークフロー
- ActivityAttributes の定義
- アクティビティのライフサイクル
- ロック画面での表示
- Dynamic Island
- プッシュによる更新
- 最近の追加項目
- よくある間違い
- レビュー用チェックリスト
- 参考資料
ワークフロー
1. 新しい Live Activity を作成する
- ホストアプリの機能と
NSSupportsLiveActivities = YESを確認します。 ActivityAttributes.ContentStateを定義し、サーバーのペイロード契約に合致する代表的なフィクスチャをエンコードおよびデコードします。ActivityConfigurationを作成し、古い状態や終了状態のコンテンツも含めて、ロック画面と Dynamic Island の状態をプレビューします。ActivityAuthorizationInfo.areActivitiesEnabledを確認してから、アクティビティをリクエストし、そのライフサイクルを監視します。- ローカル更新と、すべての終了経路をテストします。
- リモート更新では、サーバーにローテーションする更新トークンまたはプッシュ開始トークンを登録する前に、完全な参照ペイロードを検証します。
2. 既存の Live Activity コードをレビューする
このドキュメントの最後にあるレビュー用チェックリストに従ってください。
ActivityAttributes の定義
アクティビティの存続期間中に変更されない静的データと、更新ごとに変化する動的な ContentState の両方を定義します。更新やプッシュペイロードのたびに構造体全体がシリアライズされるため、ContentState は小さく保ってください。
import ActivityKit
struct DeliveryAttributes: ActivityAttributes {
// Static -- set once at activity creation, never changes
var orderNumber: Int
var restaurantName: String
// Dynamic -- updated throughout the activity lifetime
struct ContentState: Codable, Hashable {
var driverName: String
var estimatedDeliveryTime: ClosedRange<Date>
var currentStep: DeliveryStep
}
}
enum DeliveryStep: String, Codable, Hashable, CaseIterable {
case confirmed, preparing, pickedUp, delivering, delivered
var icon: String {
switch self {
case .confirmed: "checkmark.circle"
case .preparing: "frying.pan"
case .pickedUp: "bag.fill"
case .delivering: "box.truck.fill"
case .delivered: "house.fill"
}
}
}
古くなる日時
ActivityContent に staleDate を設定し、コンテンツが古くなる日時をシステムに伝えます。この日時を過ぎると、システムは context.isStale を true に設定します。ビューには代替 UI(例:「更新中…」)を表示してください。
let content = ActivityContent(
state: state,
staleDate: Date().addingTimeInterval(300), // stale after 5 minutes
relevanceScore: 75
)
アクティビティのライフサイクル
開始
Activity.request を使って Live Activity を作成し、表示します。APNs 経由のリモート更新を有効にするには、pushType に .token を渡します。ここに示す ActivityContent のリクエストには iOS 16.2 以降が必要です。
let attributes = DeliveryAttributes(orderNumber: 42, restaurantName: "Pizza Place")
let state = DeliveryAttributes.ContentState(
driverName: "Alex",
estimatedDeliveryTime: Date()...Date().addingTimeInterval(1800),
currentStep: .preparing
)
let content = ActivityContent(state: state, staleDate: nil, relevanceScore: 75)
do {
let activity = try Activity.request(
attributes: attributes,
content: content,
pushType: .token
)
print("Started activity: \(activity.id)")
} catch {
print("Failed to start activity: \(error)")
}
更新
アプリから動的コンテンツの状態を更新します。更新と同時に目に見えるバナーとサウンドを再生するには、AlertConfiguration を使用します。
let updatedState = DeliveryAttributes.ContentState(
driverName: "Alex",
estimatedDeliveryTime: Date()...Date().addingTimeInterval(600),
currentStep: .delivering
)
let updatedContent = ActivityContent(
state: updatedState,
staleDate: Date().addingTimeInterval(300),
relevanceScore: 90
)
// Silent update
await activity.update(updatedContent)
// Update with an alert
await activity.update(updatedContent, alertConfiguration: AlertConfiguration(
title: "Order Update",
body: "Your driver is nearby!",
sound: .default
))
終了
追跡中のイベントが完了したら、アクティビティを終了します。終了したアクティビティをロック画面にどれくらい残すかは、却下ポリシーで指定します。
let finalState = DeliveryAttributes.ContentState(
driverName: "Alex",
estimatedDeliveryTime: Date()...Date(),
currentStep: .delivered
)
let finalContent = ActivityContent(state: finalState, staleDate: nil, relevanceScore: 0)
// System decides when to remove (up to 4 hours)
await activity.end(finalContent, dismissalPolicy: .default)
// Remove immediately
await activity.end(finalContent, dismissalPolicy: .immediate)
// Remove after a specific time (max 4 hours from now)
await activity.end(finalContent, dismissalPolicy: .after(Date().addingTimeInterval(3600)))
成功、ユーザーまたはアプリによるキャンセル、サインアウト/セッションの停止、回復不能なアプリエラー、サーバーの致命的な失敗など、すべての終端コード経路で必ずアクティビティを終了してください。追跡中のイベントを続行できない、または正確に表現できないとサーバーが通知した場合は、最終状態を適用または送信してアクティビティを終了し、古い進捗を表示したままにしないでください。期間に関する説明をレビューする際は、アクティブ期間(アプリまたはユーザーが先に終了しない限り最大 8 時間)、システムによって終了された後のロック画面での表示期間(さらに最大 4 時間、開始から合計 12 時間)、アプリによって終了された後に .default ポリシーで残る表示期間(終了後最大 4 時間)を区別してください。
ロック画面での表示
ロック画面は、Live Activity を表示する主要な画面です。iOS 16.1 以降を搭載したすべてのデバイスで、Live Activity はここに表示されます。まずこのレイアウトを設計し、Dynamic Island が利用できる場合は、それに合わせて調整してください。
struct DeliveryActivityWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: DeliveryAttributes.self) { context in
VStack(alignment: .leading) {
Text(context.attributes.restaurantName).font(.headline)
if context.isStale {
Label("Updating...", systemImage: "arrow.trianglehead.2.clockwise")
.foregroundStyle(.secondary)
} else {
Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true)
.monospacedDigit()
}
}
.padding()
} dynamicIsland: { context in
DynamicIsland {
DynamicIslandExpandedRegion(.center) {
Text(context.attributes.restaurantName).font(.headline)
}
DynamicIslandExpandedRegion(.trailing) {
Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true)
}
} compactLeading: {
Image(systemName: "box.truck.fill")
} compactTrailing: {
Text(timerInterval: context.state.estimatedDeliveryTime, countsDown: true)
} minimal: {
Image(systemName: "box.truck.fill")
}
}
}
}
補足アクティビティファミリー
ロック画面の表示に使える縦方向のスペースは限られています。およそ160ポイントを超える高さのレイアウトは避けてください。デフォルト以外のアダプティブレイアウトを提供する場合は、iOS 18以降で supplementalActivityFamilies を使用してください。iOS/macOSのLive Activityのサイズには .medium、watchOSのLive Activityのサイズには .small を指定します。
ActivityConfiguration(for: DeliveryAttributes.self) { context in
// Lock Screen content
} dynamicIsland: { context in
// Dynamic Island
}
.supplementalActivityFamilies([.medium, .small])
Dynamic Island
Dynamic Islandの表示は、Dynamic Islandを搭載したデバイスでのみ表示されます。3つのモードすべてをデザインしてください。ただし、Dynamic Islandを搭載していないデバイスもあるため、ロック画面を主要な表示面として扱います。
コンパクト(先頭 + 末尾)
1つのLive ActivityがDynamic Islandのコンパクト領域を占有する場合に使われます。使えるスペースは非常に限られているため、最も重要な情報だけを表示します。
| 領域 | 目的 |
|---|---|
compactLeading | アクティビティを識別するアイコンまたは短いラベル |
compactTrailing | 重要な値を1つ(タイマー、スコア、ステータス) |
ミニマル
複数のLive Activityがスペースを取り合う場合に表示されます。ミニマル領域を取得できるアクティビティは1つだけです。単一のアイコンまたはグリフを表示します。
展開領域
ユーザーがDynamic Islandを長押しすると表示されます。
| 領域 | 位置 |
|---|---|
.leading | TrueDepthカメラの左側。下に回り込みます |
.trailing | TrueDepthカメラの右側。下に回り込みます |
.center | カメラの真下 |
.bottom | ほかのすべての領域の下 |
キーラインの色合い
Dynamic Islandの枠に控えめな色合いを適用します。
DynamicIsland { /* expanded */ }
compactLeading: { /* ... */ }
compactTrailing: { /* ... */ }
minimal: { /* ... */ }
.keylineTint(.blue)
プッシュによる更新
プッシュによる更新では、APNsを通じてLive Activityを更新します。アプリからのポーリングより効率的で、アプリがサスペンドされている場合も動作します。ただし、APNsの配信、優先度、予算、スロットリングの影響を受けます。
セットアップ
アクティビティの開始時に pushType として .token を渡し、アクティビティごとの更新トークンをサーバーに転送します。更新トークンは変更されることがあるため、activity.pushTokenUpdates を監視し、通知されるトークンを毎回登録し直してください。
let activity = try Activity.request(
attributes: attributes,
content: content,
pushType: .token
)
// Observe token changes -- tokens can rotate
Task {
for await token in activity.pushTokenUpdates {
let tokenString = token.map { String(format: "%02x", $0) }.joined()
try await ServerAPI.shared.registerActivityToken(
tokenString, activityID: activity.id
)
}
}
APNsペイロードの形式
次のヘッダーとJSON本文を使って、APNsにHTTP/2 POSTを送信します。
必須のデバイストークンHTTPヘッダー:
apns-push-type: liveactivityapns-topic: <bundle-id>.push-type.liveactivityapns-priority: 5(優先度低)または10(即時。予算を消費します)
aps.alert ペイロードは、表示されるアラート、バナー、サウンドの動作を制御します。優先度を指定するだけではアラートは作成されません。
timestamp、event、完全な content-state は aps 内に設定します。更新、終了、プッシュによる開始の各本文は、プッシュによる更新のペイロードにある完全な例と照合して検証してください。Codable の日付/範囲表現も正確に確認します。
プッシュによる開始
アプリを起動せずにLive Activityをリモートで開始します(iOS 17.2以降)。プッシュによる開始トークンは、Activity<Attributes>.pushToStartTokenUpdates から取得するActivityKit固有のトークンです。通常のアプリ/デバイスAPNsトークンや、アクティビティごとの更新トークンとは異なります。
Task {
for await token in Activity<DeliveryAttributes>.pushToStartTokenUpdates {
let tokenString = token.map { String(format: "%02x", $0) }.joined()
try await ServerAPI.shared.registerPushToStartToken(tokenString)
}
}
頻繁なプッシュ更新
Info.plistに NSSupportsLiveActivitiesFrequentUpdates = YES を追加すると、システムが管理するプッシュ更新の予算を増やせます。更新頻度が重要な場合は、ActivityAuthorizationInfo.frequentPushesEnabled を確認し、frequentPushEnablementUpdates を監視してください。Appleは一定の更新レートを保証していません。
最近の追加機能
スケジュール済みLive Activity(iOS 26以降)
Live Activityの開始を将来の時刻にスケジュールします。アプリがフォアグラウンドにない場合でも、システムがアクティビティを自動的に開始します。開始時刻が決まっているイベント(スポーツの試合、フライト、予定された配送)に使用します。
let scheduledDate = Calendar.current.date(
from: DateComponents(year: 2026, month: 3, day: 15, hour: 19, minute: 0)
)!
let activity = try Activity.request(
attributes: attributes,
content: content,
pushType: .token,
style: .standard,
alertConfiguration: AlertConfiguration(
title: "Game Starting",
body: "The live score is ready.",
sound: .default
),
start: scheduledDate
)
ActivityStyle(iOS 18以降のリクエストパラメーター)
iOS 18以降のリクエストパラメーター style: を使って、表示の持続動作を選択します。配送、乗車、スポーツのスコア、タイマー、フライト/ステータスボードなど、持続的なLive Activityには .standard を使用します。.transient は、Dynamic Islandの短時間の展開表示にのみ使用してください。ユーザーがデバイスをロックしたとき、展開表示を折りたたむか縮小したとき、アプリを離れたとき、またはDynamic Island以外で別の操作を行ったときに、自動的に終了することがあります。
let activity = try Activity.request(
attributes: attributes,
content: content,
pushType: .token,
style: .standard
)
ペアリング済みの Mac と CarPlay(iOS 26 以降)
ライブアクティビティは、ペアリング済みの Mac や CarPlay のホーム画面に表示できます。追加の ActivityKit API は必要ありませんが、コンパクトなレイアウトを検証してください。ライブアクティビティのボタンやトグルは、CarPlay ではアクションを実行しません。
チャネルベースのプッシュ通知(iOS 18 以降)
APNs で作成したチャネル ID を使うと、複数のライブアクティビティを一度にブロードキャスト更新できます。Xcode の外部でブロードキャスト機能を有効にし、サーバーでチャネルを作成してから、.channel(channelID) で登録します。チャネルプッシュはライブアクティビティを更新または終了しますが、開始はしません。チャネルプッシュでは、上記のデバイストークン用 apns-topic の例ではなく、apns-channel-id と有効期限を使用してください。
let activity = try Activity.request(
attributes: attributes, content: content,
pushType: .channel(channelIDFromServer)
)
よくある間違い
避けること: コンパクト表示に情報を詰め込みすぎること。表示領域は非常に小さいです。 推奨: コンパクト表示の先頭/末尾には、最も重要な情報(アイコンと値 1 つ)のみを表示します。
避けること: アプリからライブアクティビティを頻繁に更新すること(バッテリーを消耗します)。 推奨: サーバー主導の更新にはプッシュ通知を使用します。アプリ側の更新はユーザー操作に限定してください。
避けること: イベントが終了状態に達したとき、アクティビティを終了し忘れること。 推奨: 成功、キャンセル、サインアウト、回復不能なエラー、サーバー側の最終的な失敗時にアクティビティを終了します。終了されずに残ったアクティビティは、ユーザーを困らせます。
避けること: すべてのデバイスに Dynamic Island があると思い込むこと。 推奨: ロック画面を主要な表示面として設計し、Dynamic Island は補助的な表示面として扱います。
避けること: 機密情報を ActivityAttributes に保存すること(ロック画面に表示されます)。 推奨: 機密データはアプリ内に保持し、安全に表示できる概要のみを表示します。
避けること: 古い日付の処理を忘れること。
推奨: ビューで context.isStale を確認し、代替 UI(「更新中…」など)を表示します。
避けること: プッシュトークンのローテーションを無視すること。トークンはいつでも変わる可能性があります。
推奨: activity.pushTokenUpdates の非同期シーケンスを使用し、値が通知されるたびに再登録します。
避けること: NSSupportsLiveActivities の Info.plist キーを設定し忘れること。
推奨: ホストアプリの Info.plist に NSSupportsLiveActivities = YES を追加します(拡張機能ではありません)。
避けること: リクエスト/更新/終了に、非推奨の contentState ベースの API を使用すること。
推奨: すべてのライフサイクル呼び出しで ActivityContent を使用します。
避けること: ライブアクティビティのビューからネットワークデータや位置情報を直接取得すること。 推奨: 表示値をアプリまたはサーバーで事前に計算し、ActivityKit の更新またはプッシュ通知を通じて渡します。
レビューチェックリスト
-
ActivityAttributesで静的プロパティとContentStateを定義している - ホストアプリの Info.plist で
NSSupportsLiveActivities = YESを設定している - アクティビティで
ActivityContentを使用している(非推奨の contentState API ではない) - すべての終了パス(成功、エラー、キャンセル、サインアウト、サーバーの致命的な障害)でアクティビティを終了している
- ActivityKit のライフサイクルとロック画面/Dynamic Island の Live Activity 表示面を、通常のホーム画面/タイムラインウィジェットの作業と分けている
- ロック画面のレイアウト(Live Activity の主要な表示面)で
context.isStaleを処理している - Dynamic Island のコンパクト、展開、最小表示を実装し、ロック画面へのフォールバックを用意している
- プッシュ更新トークンを
activity.pushTokenUpdates経由でサーバーに転送している -
Activity<Attributes>.pushToStartTokenUpdates経由でプッシュ開始トークンを取得している - プッシュ開始ペイロードに必須の
alertを含めている -
content-stateJSON が、連携した日付/範囲のエンコードを含め、実際のContentStateのCodable形式と一致している - 8 時間のアクティブ期間、システムによって終了された場合のロック画面上での合計 12 時間の表示、アプリによって終了された場合の
.defaultによる 4 時間の表示を区別して確認している - 開始前に
ActivityAuthorizationInfoを確認している - 高頻度のプッシュを前提とする前に
frequentPushesEnabledを確認している - 更新のたびにシリアライズされる
ContentStateを小さく保っている -
style:、.channel、追加のファミリーで iOS 18 以降の利用可能性を確認している - iOS 18 以降の
style:の選択に根拠がある。永続的な Live Activity には.standardを使用し、短時間の Dynamic Island 展開表示に限り.transientを使用している - ActivityKit のプッシュ優先度と
aps.alertの動作を個別に処理している - Live Activity のビューでネットワークや位置情報に関する処理を直接行っていない
- プッシュ配信と Dynamic Island の動作を実機でテストしている
参考資料
- パターンとコード例については、references/activitykit-patterns.md を参照してください