activitykit

activitykit

热门

使用 ActivityKit 在 iOS 应用中实现、审查或优化实时活动(Live Activities)与灵动岛(Dynamic Island)体验。适用于为锁屏界面和灵动岛构建实时更新的小组件——例如外卖/快递追踪、体育赛事比分、打车状态、健身计时器、媒体播放控制或任何需要实时更新的时效性信息。也适用于处理 ActivityKit、ActivityAttributes、Activity 生命周期(请求/更新/结束)、灵动岛布局(紧凑/最小化/展开)、远程推送更新(push-to-update)实时活动或锁屏实时小组件等开发场景。

957Star
48Fork
更新于 2026/7/31
SKILL.md
只读
名称
activitykit
描述

使用 ActivityKit 在 iOS 应用中实现、审查或优化实时活动(Live Activities)与灵动岛(Dynamic Island)体验。适用于为锁屏界面和灵动岛构建实时更新的小组件——例如外卖/快递追踪、体育赛事比分、打车状态、健身计时器、媒体播放控制或任何需要实时更新的时效性信息。也适用于处理 ActivityKit、ActivityAttributes、Activity 生命周期(请求/更新/结束)、灵动岛布局(紧凑/最小化/展开)、远程推送更新(push-to-update)实时活动或锁屏实时小组件等开发场景。

ActivityKit

ActivityKit 负责管理展示在锁屏界面和灵动岛上的实时、一目了然的实时活动(Live Activities)。普通的按时间线刷新的小组件归属于 widgetkit,而通用的 APNs 推送配置则属于 push-notifications;ActivityKit 专精于 Live Activity 的生命周期管理与数据载荷(payload)契约。aps.content-state 必须能准确解构为与 ActivityAttributes.ContentState 完全一致的数据结构,包括任何协同自定义的日期/范围编码。除特别说明外,涉及现代 ActivityContent 生命周期的示例均要求 iOS 16.2 及以上版本。

参阅 references/activitykit-patterns.md 查看完整的代码模式,包含推送载荷格式、并发活动管理、状态监听以及测试方法。

目录

开发流程

1. 创建全新的实时活动(Live Activity)

  1. 确认主应用已配置对应 Capability,且 NSSupportsLiveActivities = YES
  2. 定义 ActivityAttributes.ContentState;对符合服务端 Payload 契约的代表性 Fixture 数据进行编解码验证。
  3. 创建 ActivityConfiguration,并预览锁屏和灵动岛下的各种状态(包括数据过期状态和终态)。
  4. 检查 ActivityAuthorizationInfo.areActivitiesEnabled,随后发起请求并监听 Activity 生命周期。
  5. 验证本地更新流程以及所有终态结束路径。
  6. 对于远程更新,在向服务端注册轮换更新 Token 或远程启动 Token(push-to-start token)前,先校验一份完整的参考 Payload。

2. 审查现有的 Live Activity 代码

参照本文末尾的审查清单逐项检查。

ActivityAttributes 定义

同时定义静态数据(在 Activity 整个生命周期内不可变)和动态 ContentState(随着每次更新而改变)。保持 ContentState 体积尽量精简,因为整个结构体会在每次更新和推送 Payload 中序列化。

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 创建并展示 Live Activity。将 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)")
}

更新活动

从应用内部更新动态 ContentState。可以使用 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
)

// 静默更新
await activity.update(updatedContent)

// 附带强提醒(Alert)的更新
await activity.update(updatedContent, alertConfiguration: AlertConfiguration(
    title: "Order Update",
    body: "Your driver is nearby!",
    sound: .default
))

结束活动

当追踪的事件完成时结束 Activity。选择合适的移除策略(Dismissal Policy),控制已结束的 Activity 在锁屏界面留存的时长。

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)))

请确保在所有终态代码路径下均显式结束 Activity——包括成功完成、用户/应用主动取消、退出登录/会话终止、应用发生不可恢复的错误以及服务端终态失败。如果服务端指示被追踪的事件无法继续或无法再准确呈现,应应用或发送最终状态并结束 Activity,而不是一直展示停滞的数据进度。在梳理生命周期时,请明确区分三个阶段:活跃生命周期(最长 8 小时,除非应用或用户提前结束)、系统结束后的锁屏留存(额外最长 4 小时,即自启动起共计最长 12 小时),以及由应用手动调用结束时配合 .default 策略的留存时长(结束之后最多保留 4 小时)。

锁屏界面展示

锁屏界面是 Live Activity 的核心展示区域。所有运行 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 pt 的布局。在 iOS 18+ 上,如果需要提供除默认规格外的自适应布局,可使用 supplementalActivityFamilies.medium 用于 iOS/macOS 上的 Live Activity 尺寸,.small 用于 watchOS 上的 Live Activity 尺寸。

ActivityConfiguration(for: DeliveryAttributes.self) { context in
    // 锁屏内容
} dynamicIsland: { context in
    // 灵动岛
}
.supplementalActivityFamilies([.medium, .small])

灵动岛

灵动岛展示效果仅在配备灵动岛的设备上生效。建议完整设计这三种模式,但仍需将锁屏界面视为核心展示阵地,因为并非所有设备都支持灵动岛。

紧凑模式(Compact:左侧 + 右侧)

当单个 Live Activity 占据灵动岛紧凑空间时使用。此区域空间极其有限——仅用于展示最关键的信息。

区域 用途
compactLeading 识别活动的图标或极短文本标签
compactTrailing 单个核心数值(倒计时、比分、状态)

最小化模式(Minimal)

当多个 Live Activity 竞相占用灵动岛时显示。只有一个活动能够获得该最小化槽位。通常仅展示单一图标或符号。

展开模式区域(Expanded Regions)

当用户长按灵动岛时展开显示。

区域 位置
.leading TrueDepth 摄像头左侧;下方可换行包裹
.trailing TrueDepth 摄像头右侧;下方可换行包裹
.center 位于摄像头正下方
.bottom 位于所有其他区域的最下方

轮廓描边色调(Keyline Tint)

为灵动岛边缘添加一层微妙的色彩提亮:

DynamicIsland { /* 展开模式 */ }
    compactLeading: { /* ... */ }
    compactTrailing: { /* ... */ }
    minimal: { /* ... */ }
    .keylineTint(.blue)

远程推送更新(Push-to-Update)

Push-to-update 通过 APNs 发送 Live Activity 更新,相比应用内部轮询更加高效,且在应用处于后台挂起时依然生效,受 APNs 送达率、优先级、配额和频控限制。

初始化配置

启动活动时将 pushType 设置为 .token,随后将每个 Activity 专属的更新 Token 转发至你的服务端。更新 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 请求,并包含以下 Request Header 和 JSON Body:

基于 Device-Token 的必填 HTTP 请求头:

  • 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 中完整的 Payload 示例对更新、结束以及远程启动(push-to-start)的数据体进行校验,包括精密的 Codable 日期与范围表示法。

远程启动活动(Push-to-Start)

在应用未运行的状态下通过远程推送启动 Live Activity(iOS 17.2+)。Push-to-start Token 属于 ActivityKit 规范