live-activities について
live-activitiesは、ActivityKitの体験を構築・レビューするためのSwift開発スキルです。リアルタイム情報を表示する開発者向けに、アクティビティのライフサイクル、Dynamic Islandのレイアウト、プッシュ更新、新しいスケジュール機能や表示方法を扱います。
主な機能
- 01ActivityKitの設定とライフサイクルパターン
- 02ロック画面とDynamic Islandのレイアウト
- 03APNsによるプッシュ更新と起動
- 04スケジュール機能とチャンネル型アクティビティ
- 05コードレビュー用チェックリストと注意点
活用シーン
- 01ロック画面に配送状況を表示
- 02サーバー駆動のアクティビティ更新を送信
- 03将来のイベントに合わせてアクティビティを予約
- 04Live Activityの実装をレビュー
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill live-activitiesインストール後、Claude Code で/live-activitiesを入力して読み込まれたか確認します。
同梱ファイルreferences/live-activity-patterns.md
詳細
ライブアクティビティとDynamic Island
ActivityKitを使用して、ロック画面、Dynamic Island、 StandBy、CarPlay、Macのメニューバーで、リアルタイムに情報をひと目で確認できる体験を構築します。パターンはSwift 6.2を使用するiOS 26以降を対象としており、特に記載がない限りiOS 16.1まで下位互換性があります。
プッシュペイロード形式、複数のアクティビティの同時実行、状態の監視、テストなど、コードパターンの全体については、references/live-activity-patterns.mdを参照してください。
目次
- ワークフロー
- ActivityAttributesの定義
- アクティビティのライフサイクル
- ロック画面での表示
- Dynamic Island
- プッシュによる更新
- iOS 26での追加機能
- よくある間違い
- レビュー用チェックリスト
- 参考資料
ワークフロー
1. 新しいライブアクティビティを作成する
- ホストアプリのInfo.plistに
NSSupportsLiveActivities = YESを追加します。 - ネストされた
ContentStateを含むActivityAttributes構造体を定義します。 - ウィジェットバンドル内に、ロック画面のコンテンツとDynamic Islandのクロージャーを含む
ActivityConfigurationを作成します。 Activity.request(attributes:content:pushType:)でアクティビティを開始します。activity.update(_:)で更新し、activity.end(_:dismissalPolicy:)で終了します。- プッシュトークンをサーバーに転送して、リモート更新できるようにします。
2. 既存のライブアクティビティコードをレビューする
このドキュメントの最後にあるレビュー用チェックリストに従って確認します。
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を使用してライブアクティビティを作成し、表示します。APNs経由のリモート更新を有効にするには、pushTypeに.tokenを渡します。
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時間)ロック画面に残り、ユーザーの不満につながります。
ロック画面での表示
ロック画面はライブアクティビティの主要な表示場所です。iOS 16.1以降を搭載したすべてのデバイスで、ここにライブアクティビティが表示されます。まずこのレイアウトを設計してください。
struct DeliveryActivityWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: DeliveryAttributes.self) { context in
// Lock Screen / StandBy / CarPlay / Mac menu bar content
VStack(alignment: .leading, spacing: 8) {
HStack {
Text(context.attributes.restaurantName)
.font(.headline)
Spacer()
Text("Order #\(context.attributes.orderNumber)")
.font(.caption)
.foregroundStyle(.secondary)
}
if context.isStale {
Label("Updating...", systemImage: "arrow.trianglehead.2.clockwise")
.font(.subheadline)
.foregroundStyle(.secondary)
} else {
HStack {
Label(context.state.driverName, systemImage: "person.fill")
Spacer()
Text(timerInterval: context.state.estimatedDeliveryTime,
countsDown: true)
.monospacedDigit()
}
.font(.subheadline)
// Progress steps
HStack(spacing: 12) {
ForEach(DeliveryStep.allCases, id: \.self) { step in
Image(systemName: step.icon)
.foregroundStyle(
step <= context.state.currentStep ? .primary : .tertiary
)
}
}
}
}
.padding()
} dynamicIsland: { context in
// Dynamic Island closures (see next section)
DynamicIsland {
// Expanded regions...
DynamicIslandExpandedRegion(.leading) {
Image(systemName: "box.truck.fill").font(.title2)
}
DynamicIslandExpandedRegion(.trailing) {
Text(timerInterval: context.state.estimatedDeliveryTime,
countsDown: true)
.font(.caption).monospacedDigit()
}
DynamicIslandExpandedRegion(.center) {
Text(context.attributes.restaurantName).font(.headline)
}
DynamicIslandExpandedRegion(.bottom) {
HStack(spacing: 12) {
ForEach(DeliveryStep.allCases, id: \.self) { step in
Image(systemName: step.icon)
.foregroundStyle(
step <= context.state.currentStep ? .primary : .tertiary
)
}
}
}
} compactLeading: {
Image(systemName: "box.truck.fill")
} compactTrailing: {
Text(timerInterval: context.state.estimatedDeliveryTime,
countsDown: true)
.frame(width: 40).monospacedDigit()
} minimal: {
Image(systemName: "box.truck.fill")
}
}
}
}
ロック画面のサイズ
ロック画面で表示できる縦方向のスペースは限られています。およそ160ポイントを超える高さのレイアウトは避けてください。supplementalActivityFamiliesを使用して、.small(コンパクト)または.medium(標準)のサイズに対応します。
ActivityConfiguration(for: DeliveryAttributes.self) { context in
// Lock Screen content
} dynamicIsland: { context in
// Dynamic Island
}
.supplementalActivityFamilies([.small, .medium])
Dynamic Island
Dynamic IslandはiPhone 14 Pro以降で利用できます。表示モードは3種類あります。3種類すべてを設計しますが、すべてのデバイスにDynamic Islandがあるわけではないため、ロック画面を主要な表示場所として扱ってください。
コンパクト(Leading + Trailing)
ライブアクティビティが1つだけ有効な場合、常に表示されます。スペースが非常に限られているため、最も重要な情報だけを表示します。
| 領域 | 用途 |
|---|---|
compactLeading | アクティビティを識別するアイコンまたは短いラベル |
compactTrailing | 重要な値を1つ(タイマー、スコア、ステータス) |
ミニマル
複数のライブアクティビティがスペースを取り合う場合に表示されます。ミニマル表示枠を利用できるアクティビティは1つだけです。アイコンまたはグリフを1つ表示します。
展開領域
ユーザーが Dynamic Island を長押しすると表示されます。
| 領域 | 位置 |
|---|---|
.leading | TrueDepth カメラの左側。下に折り返します |
.trailing | TrueDepth カメラの右側。下に折り返します |
.center | カメラのすぐ下 |
.bottom | 他のすべての領域の下 |
キーラインの色合い
Dynamic Island の境界に控えめな色合いを適用します。
DynamicIsland { /* expanded */ }
compactLeading: { /* ... */ }
compactTrailing: { /* ... */ }
minimal: { /* ... */ }
.keylineTint(.blue)
プッシュによる更新
プッシュによる更新では、APNs 経由で Live Activity を更新します。アプリからポーリングするより効率的で、アプリが一時停止中でも動作します。
セットアップ
アクティビティの開始時に pushType として .token を渡し、プッシュトークンをサーバーに転送します。
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": {
"timestamp": 1700000000,
"event": "update",
"content-state": {
"driverName": "Alex",
"estimatedDeliveryTime": {
"lowerBound": 1700000000,
"upperBound": 1700001800
},
"currentStep": "delivering"
},
"stale-date": 1700000300,
"alert": {
"title": "Delivery Update",
"body": "Your driver is nearby!"
}
}
}
終了ペイロード: "event": "end" と任意の "dismissal-date" を指定した、同じ構造です。
content-state JSON は、ContentState の Codable 構造体と完全に一致している必要があります。キーや型が一致しないと、エラーが通知されずに失敗します。
プッシュによる開始
アプリを起動せずに、リモートから Live Activity を開始します(iOS 17.2 以降)。
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 を追加すると、プッシュ更新の予算を増やせます。1 分に 1 回を超えて更新されるアクティビティ(スポーツのスコア、ライドの追跡など)で使用します。
iOS 26 での追加機能
スケジュールされた 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,
start: scheduledDate
)
ActivityStyle(iOS 16.1 以降の型、iOS 26 以降の style: パラメーター)
.standard(終了するまで持続するデフォルト)または .transient(システムが自動的に閉じる場合があります)を指定して、表示の持続性を制御します。公共交通機関の到着案内など、短時間の更新には .transient を使用します。Activity.request の style: パラメーターには iOS 26 以降が必要です。
let activity = try Activity.request(
attributes: attributes, content: content,
pushType: .token, style: .transient
)
Mac のメニューバーと CarPlay(iOS 26 以降)
Live Activity は macOS Tahoe のメニューバー(iPhone ミラーリング経由)と CarPlay のホーム画面に自動的に表示されます。追加のコードは必要ありません。ロック画面のレイアウトが小さいサイズでも読みやすいことを確認してください。
チャネルベースのプッシュ(iOS 18 以降)
.channel を使用すると、多数の Live Activity に一度に更新を配信できます。
let activity = try Activity.request(
attributes: attributes, content: content,
pushType: .channel("delivery-updates")
)
よくある間違い
やってはいけないこと: コンパクト表示に情報を詰め込みすぎる。表示領域は非常に小さいです。 推奨: コンパクト表示の先頭と末尾には、最も重要な情報(アイコン+値1つ)のみを表示します。
やってはいけないこと: アプリからLive Activitiesを頻繁に更新する(バッテリーを消耗します)。 推奨: サーバー主導の更新にはプッシュ更新を使用します。アプリ側での更新はユーザー操作に限定します。
やってはいけないこと: イベント完了時にアクティビティを終了し忘れる。 推奨: 成功、エラー、キャンセルのすべての経路で、必ずアクティビティを終了します。アクティビティが残ったままだと、ユーザーに不便をかけます。
やってはいけないこと: Dynamic Islandが常に利用できると思い込む(iPhone 14 Pro以降のみ)。 推奨: ロック画面を主要な表示面として設計し、Dynamic Islandは補助的なものとして扱います。
やってはいけないこと: 機密情報をActivityAttributesに保存する(ロック画面に表示されます)。 推奨: 機密データはアプリ内に保持し、安全に表示できる概要のみを表示します。
やってはいけないこと: 古くなった日付への対応を忘れる。
推奨: ビューでcontext.isStaleを確認し、フォールバックUI(「更新中…」など)を表示します。
やってはいけないこと: プッシュトークンのローテーションを無視する。トークンはいつでも変更される可能性があります。
推奨: activity.pushTokenUpdatesの非同期シーケンスを使用し、値が出力されるたびに再登録します。
やってはいけないこと: NSSupportsLiveActivitiesのInfo.plistキーを設定し忘れる。
推奨: ホストアプリのInfo.plistにNSSupportsLiveActivities = YESを追加します(拡張機能ではありません)。
やってはいけないこと: リクエスト/更新/終了に非推奨のcontentStateベースのAPIを使用する。
推奨: すべてのライフサイクル呼び出しにActivityContentを使用します。
やってはいけないこと: Live Activityのビューに重いロジックを入れる。サイズが制限されたウィジェットプロセスでレンダリングされます。
推奨: 表示値を事前に計算し、ContentStateを通じて渡します。
レビューチェックリスト
-
ActivityAttributesで静的プロパティとContentStateを定義している - ホストアプリのInfo.plistに
NSSupportsLiveActivities = YESが設定されている - アクティビティで
ActivityContentを使用している(非推奨のcontentState APIではない) - すべてのコードパス(成功、エラー、キャンセル)でアクティビティを終了している
- ロック画面のレイアウトで
context.isStaleに対応している - Dynamic Islandのコンパクト表示、展開表示、最小表示を実装している
-
activity.pushTokenUpdatesを介してプッシュトークンをサーバーに転送している - 重要な更新に
AlertConfigurationを使用している - 開始前に
ActivityAuthorizationInfoを確認している - ContentStateを小さく保っている(更新のたびにシリアライズされます)
- 実機でテストしている(Dynamic Islandの表示はSimulatorと異なります)
- ActivityAttributesとContentStateの型がSendableであることを確認し、Live ActivityのUIを@MainActorで更新している
参考資料
- パターンとコード例については、
references/live-activity-patterns.mdを参照してください