
activitykit
熱門使用 ActivityKit 在 iOS App 中實作、審查或改進即時動態(Live Activity)與動態島(Dynamic Island)體驗。適用於為鎖定畫面(Lock Screen)與動態島打造即時更新的小工具——例如外送追蹤、體育賽事比分、叫車服務狀態、健身計時器、媒體播放,或任何需要即時更新的時間敏感資訊。此外,也可用於處理 ActivityKit、ActivityAttributes、Activity 生命週期(request/update/end)、動態島版面配置(compact/minimal/expanded)、推播更新即時動態(push-to-update Live Activities)或鎖定畫面即時小工具等情境。
使用 ActivityKit 在 iOS App 中實作、審查或改進即時動態(Live Activity)與動態島(Dynamic Island)體驗。適用於為鎖定畫面(Lock Screen)與動態島打造即時更新的小工具——例如外送追蹤、體育賽事比分、叫車服務狀態、健身計時器、媒體播放,或任何需要即時更新的時間敏感資訊。此外,也可用於處理 ActivityKit、ActivityAttributes、Activity 生命週期(request/update/end)、動態島版面配置(compact/minimal/expanded)、推播更新即時動態(push-to-update Live Activities)或鎖定畫面即時小工具等情境。
ActivityKit
ActivityKit 負責處理顯示在鎖定畫面(Lock Screen)和動態島(Dynamic Island)上、可一目瞭然的即時動態(Live Activity)。一般的時間軸小工具(Timeline widgets)屬於 widgetkit 的範疇,而通用的 APNs 設定則歸 push-notifications 處理;ActivityKit 專注於即時動態的生命週期與 Payload 契約(Payload contract)。aps.content-state 必須能精確解碼為 ActivityAttributes.ContentState 的型別結構,包含任何自訂的日期/範圍編碼協調。除非另有說明,否則現代 ActivityContent 生命週期範例均需要 iOS 16.2+。
請參閱 references/activitykit-patterns.md 以取得完整的程式碼模式,包含推播 Payload 格式、並列執行的即時動態、狀態監聽與測試。
目錄
工作流程
1. 建立新的即時動態
- 確認主 App 的 Capability 已啟用且設定
NSSupportsLiveActivities = YES。 - 定義
ActivityAttributes.ContentState;編碼並解碼具代表性的 Fixture 測試資料,以符合伺服器端的 Payload 契約。 - 建立
ActivityConfiguration,並預覽鎖定畫面與動態島的各種狀態(包含資料過期與終止狀態)。 - 檢查
ActivityAuthorizationInfo.areActivitiesEnabled,接著請求建立並監聽即時動態的生命週期。 - 測試本地端更新以及所有終止(end)路徑。
- 若使用遠端更新,在向伺服器註冊輪替更新代碼(rotating update tokens)或推播啟動代碼(push-to-start tokens)前,請先驗證完整的參考 Payload。
2. 審查現有的即時動態程式碼
請依序對照本文件末尾的審查檢核表進行檢查。
ActivityAttributes 定義
同時定義靜態資料(在即時動態的生命週期中不可變)與動態的 ContentState(每次更新時改變)。請保持 ContentState 儘量輕巧,因為每次更新與推播 Payload 都會對整個 Struct 進行序列化。
import ActivityKit
struct DeliveryAttributes: ActivityAttributes {
// 靜態資料 -- 在建立即時動態時設定一次,之後絕不改變
var orderNumber: Int
var restaurantName: String
// 動態資料 -- 在即時動態生命週期中持續更新
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"
}
}
}
資料過期時間(Stale Date)
在 ActivityContent 上設定 staleDate,以告知系統內容何時過期。超過此日期後,系統會將 context.isStale 設為 true;您可以在 UI 中顯示備用畫面(例如:「更新中...」)。
let content = ActivityContent(
state: state,
staleDate: Date().addingTimeInterval(300), // 5 分鐘後過期
relevanceScore: 75
)
Activity 生命週期
開始
使用 Activity.request 來建立並顯示即時動態。將 pushType 傳入 .token,即可透過 APNs 啟用遠端更新。此處展示的 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)")
}
更新
從 App 更新動態內容狀態。搭配使用 AlertConfiguration,可在更新時一併觸發可見的橫幅(Banner)與音效。
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)
// 帶有提示與橫幅的更新
await activity.update(updatedContent, alertConfiguration: AlertConfiguration(
title: "Order Update",
body: "Your driver is nearby!",
sound: .default
))
結束
當追蹤的事件完成時,請結束即時動態。選擇合適的關閉策略(dismissal policy),以控制已結束的即時動態在鎖定畫面上停留的時間。
let finalState = DeliveryAttributes.ContentState(
driverName: "Alex",
estimatedDeliveryTime: Date()...Date(),
currentStep: .delivered
)
let finalContent = ActivityContent(state: finalState, staleDate: nil, relevanceScore: 0)
// 由系統決定移除時間(最多 4 小時)
await activity.end(finalContent, dismissalPolicy: .default)
// 立即移除
await activity.end(finalContent, dismissalPolicy: .immediate)
// 指定時間後移除(自目前起最多 4 小時)
await activity.end(finalContent, dismissalPolicy: .after(Date().addingTimeInterval(3600)))
請務必在所有終止程式碼路徑中結束即時動態——包括成功完成、使用者/App 取消、登出/工作階段停止、無法復原的 App 錯誤以及伺服器端終止失敗。若伺服器指示追蹤的事件無法繼續或無法再精確呈現,請套用或傳送最終的終止狀態並結束即時動態,而非留下過期的進度畫面。在審查持續時間聲明時,請區分:作用中生命週期(最長可達 8 小時,除非 App 或使用者提早結束)、由系統結束在鎖定畫面的停留時間(最多可額外停留 4 小時,自啟動起總計最多 12 小時),以及 App 以 .default 關閉策略結束後的停留時間(結束後最多停留 4 小時)。
鎖定畫面呈現
鎖定畫面是即時動態最主要的展示介面。所有搭載 iOS 16.1+ 的裝置都會在此顯示即時動態。請優先設計此版面,再針對支援動態島的裝置進行調整。
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")
}
}
}
}
補充即時動態系列(Supplemental Activity Families)
鎖定畫面的垂直展示空間有限。請避免設計高度超過約 160 點(points)的版面。在 iOS 18+ 上,當您要提供預設規格之外的自適應版面時,請使用 supplementalActivityFamilies:其中 .medium 適用於 iOS/macOS 的即時動態尺寸,.small 適用於 watchOS 的即時動態尺寸。
ActivityConfiguration(for: DeliveryAttributes.self) { context in
// 鎖定畫面內容
} dynamicIsland: { context in
// 動態島
}
.supplementalActivityFamilies([.medium, .small])
動態島
動態島呈現方式僅會出現在配備動態島的裝置上。請完整設計這三種模式,但仍需將鎖定畫面視為最主要的展示介面,因為並非所有裝置都支援動態島。
緊湊模式(Compact: 導前 + 導後 / Leading + Trailing)
用於當單一即時動態佔用動態島的緊湊空間時。此處的空間極度有限——僅顯示最重要的核心資訊。
| 區域 | 用途 |
|---|---|
compactLeading |
識別該即時動態的圖示或微型標籤 |
compactTrailing |
單一核心數值(計時器、比分、狀態) |
最小化模式(Minimal)
用於有多個即時動態同時爭用空間時。只有一個即時動態能取得最小化版位,請僅顯示單一圖示或符號。
展開區域(Expanded Regions)
當使用者長按動態島時顯示。
| 區域 | 位置 |
|---|---|
.leading |
原深感測鏡頭(TrueDepth camera)左側;向下延伸包覆 |
.trailing |
原深感測鏡頭右側;向下延伸包覆 |
.center |
鏡頭正下方 |
.bottom |
所有其他區域下方 |
邊框色調(Keyline Tint)
為動態島邊框套用微妙的微調色彩:
DynamicIsland { /* 展開模式 */ }
compactLeading: { /* ... */ }
compactTrailing: { /* ... */ }
minimal: { /* ... */ }
.keylineTint(.blue)
推播更新(Push-to-Update)
推播更新(Push-to-update)透過 APNs 傳送即時動態更新,這比從 App 進行輪詢(polling)更加高效,且在 App 處於背景暫停狀態下仍可運作,但受限於 APNs 的送達率、優先順序、配額(budget)與流量限制(throttling)。
設定
啟動即時動態時,將 pushType 設定為 .token,隨後將各即時動態專屬的更新代碼(update token)傳送至您的伺服器。更新代碼可能會定期輪替,因此請監聽 activity.pushTokenUpdates 並重新註冊每個發出的 token:
let activity = try Activity.request(
attributes: attributes,
content: content,
pushType: .token
)
// 監聽 token 變更 -- token 可能會輪替
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 Payload 格式
向 APNs 發送 HTTP/2 POST 請求,並附帶以下 Header 與 JSON Body:
必要的 Device Token HTTP Header:
apns-push-type: liveactivityapns-topic: <bundle-id>.push-type.liveactivityapns-priority: 5(較低優先權)或10(立即送達,會扣減配額)
aps.alert Payload 控制可見的提示/橫幅/音效行為;僅設定優先權本身不會觸發提示橫幅。
將 timestamp、event 以及完整的 content-state 放進 aps 內部。請對照 Push-to-Update Payloads 中完整的範例(包含精確的 Codable 日期/範圍表示法),驗證更新、結束和 Push-to-start 的 Body 內容。
推播啟動(Push-to-Start)
在 App 未執行的狀態下從遠端啟動即時動態(iOS 17.2+)。Push-to-start token 是 ActivityKit-spec



