activitykit

activitykit

熱門

使用 ActivityKit 在 iOS App 中實作、審查或改進即時動態(Live Activity)與動態島(Dynamic Island)體驗。適用於為鎖定畫面(Lock Screen)與動態島打造即時更新的小工具——例如外送追蹤、體育賽事比分、叫車服務狀態、健身計時器、媒體播放,或任何需要即時更新的時間敏感資訊。此外,也可用於處理 ActivityKit、ActivityAttributes、Activity 生命週期(request/update/end)、動態島版面配置(compact/minimal/expanded)、推播更新即時動態(push-to-update Live Activities)或鎖定畫面即時小工具等情境。

957星標
48分支
更新於 2026/7/31
SKILL.md
唯讀
名稱
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

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. 建立新的即時動態

  1. 確認主 App 的 Capability 已啟用且設定 NSSupportsLiveActivities = YES
  2. 定義 ActivityAttributes.ContentState;編碼並解碼具代表性的 Fixture 測試資料,以符合伺服器端的 Payload 契約。
  3. 建立 ActivityConfiguration,並預覽鎖定畫面與動態島的各種狀態(包含資料過期與終止狀態)。
  4. 檢查 ActivityAuthorizationInfo.areActivitiesEnabled,接著請求建立並監聽即時動態的生命週期。
  5. 測試本地端更新以及所有終止(end)路徑。
  6. 若使用遠端更新,在向伺服器註冊輪替更新代碼(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: liveactivity
  • apns-topic: <bundle-id>.push-type.liveactivity
  • apns-priority: 5(較低優先權)或 10(立即送達,會扣減配額)

aps.alert Payload 控制可見的提示/橫幅/音效行為;僅設定優先權本身不會觸發提示橫幅。

timestampevent 以及完整的 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