mcpField
A
activitykit

activitykit

github.com/dpearson2699开发1.2k3.4k 次安装13 周前更新
代码仓库

关于 activitykit

ActivityKit 是用于实现锁定屏幕和 Dynamic Island 实时活动的开发技能,面向 iOS 开发者。它涵盖状态与生命周期、界面呈现及 APNs 推送更新,并指导验证负载格式。

主要功能

  1. 01定义静态属性和动态状态
  2. 02创建、更新和结束活动
  3. 03设计锁定屏幕与 Dynamic Island 布局
  4. 04通过 APNs 发送更新
  5. 05处理定时启动与频道推送活动

适用场景

  1. 01在锁定屏幕显示配送进度
  2. 02在 Dynamic Island 显示实时比分
  3. 03根据服务器信息更新活动状态
  4. 04为已知时间的事件安排活动

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill activitykit

安装后在 Claude Code 中输入/activitykit确认已加载。

附带文件evals/evals.jsonreferences/activitykit-patterns.md

详细介绍

ActivityKit

ActivityKit 管理在锁定屏幕和灵动岛上显示的实时、一目了然的 Live Activities。普通时间线小组件应放在 widgetkit 中,通用 APNs 配置应放在 push-notifications 中;ActivityKit 管理 Live Activity 生命周期和载荷契约。aps.content-state 必须解码为与 ActivityAttributes.ContentState 完全一致的结构,包括任何协同使用的自定义日期/范围编码。除非另有说明,现代 ActivityContent 生命周期示例要求 iOS 16.2+。

请参阅 references/activitykit-patterns.md,了解完整代码模式,包括推送载荷格式、并发活动、状态观察和测试。

目录

工作流

1. 创建新的 Live Activity
  1. 确认宿主应用的功能配置以及 NSSupportsLiveActivities = YES。
  2. 定义 ActivityAttributes.ContentState;对符合服务端载荷契约的代表性样例进行编码和解码。
  3. 创建 ActivityConfiguration,并预览锁定屏幕和灵动岛上的状态,包括过期内容和终止内容。
  4. 检查 ActivityAuthorizationInfo.areActivitiesEnabled,然后请求并观察活动生命周期。
  5. 测试本地更新和所有终止路径。
  6. 对于远程更新,在向服务端注册轮换更新令牌或推送启动令牌之前,先验证完整的参考载荷。
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;请在视图中显示备用界面(例如“正在更新……”)。

let content = ActivityContent(
    state: state,
    staleDate: Date().addingTimeInterval(300), // stale after 5 minutes
    relevanceScore: 75
)

Activity 生命周期

启动

使用 Activity.request 创建并显示 Live Activity。将 .token 作为 pushType 传入,以启用通过 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)")
}
更新

从应用更新动态内容状态。使用 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 Activities。请先设计此布局,再根据需要适配灵动岛。

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:.medium 用于 iOS/macOS 实时活动尺寸,.small 用于 watchOS 实时活动尺寸。

ActivityConfiguration(for: DeliveryAttributes.self) { context in
    // Lock Screen content
} dynamicIsland: { context in
    // Dynamic Island
}
.supplementalActivityFamilies([.medium, .small])

灵动岛

灵动岛展示仅会出现在配备灵动岛的设备上。请设计全部三种模式,但将锁定屏幕视为主要界面,因为并非所有设备都配备灵动岛。

紧凑模式(前导 + 尾随)

当一项实时活动占用灵动岛紧凑空间时使用。可用空间极其有限——只显示最关键的信息。

区域用途
compactLeading用于标识活动的图标或极短标签
compactTrailing一个关键值(计时器、分数、状态)
最小模式

当多项实时活动争用空间时显示。只有一项活动能占用最小模式区域。显示单个图标或字形。

展开区域

用户长按灵动岛时显示。

区域位置
.leadingTrueDepth 摄像头左侧;可向下换行
.trailingTrueDepth 摄像头右侧;可向下换行
.center摄像头正下方
.bottom所有其他区域的下方
关键线颜色

为灵动岛边框应用微妙的色调:

DynamicIsland { /* expanded */ }
    compactLeading: { /* ... */ }
    compactTrailing: { /* ... */ }
    minimal: { /* ... */ }
    .keylineTint(.blue)

推送更新

推送更新通过 APNs 发送实时活动更新,比应用轮询更高效,并且在应用暂停时也能工作,但受 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: liveactivity
  • apns-topic: <bundle-id>.push-type.liveactivity
  • apns-priority: 5(较低优先级)或 10(立即发送,会计入预算)

aps.alert 负载控制可见提醒、横幅和声音行为;仅设置优先级不会触发提醒。

将 timestamp、event 和完整的 content-state 放在 aps 中。请根据推送更新负载中的完整示例,验证更新、结束和推送启动正文,包括准确的 Codable 日期/范围表示形式。

推送启动

无需运行应用即可远程启动实时活动(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 不保证固定的更新速率。

最新功能

计划的实时活动(iOS 26 及更高版本)

将实时活动计划为在未来某个时间开始。即使应用未在前台运行,系统也会自动启动活动。适用于已知开始时间的事件(体育比赛、航班、计划配送)。

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: 请求参数选择持久化行为。配送、出行、体育比分、计时器和航班/状态看板等持久实时活动,请使用 .standard。仅在短暂展示展开的灵动岛时使用 .transient;当用户锁定设备、收起或缩小展开的展示、离开应用,或在灵动岛之外执行其他操作时,它可能会自动结束。

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-channel-id 和过期时间,而不是上文设备令牌示例中的 apns-topic。

let activity = try Activity.request(
    attributes: attributes, content: content,
    pushType: .channel(channelIDFromServer)
)

常见错误

不要: 在紧凑显示中放入太多内容——空间很小。 要: 仅在紧凑显示的前导/尾随区域展示最关键的信息(图标 + 一个数值)。

不要: 在应用中更新实时活动过于频繁(会消耗电池电量)。 要: 对由服务端驱动的更新使用推送更新。将应用端更新限制在用户操作时。

不要: 忘记在事件进入任何终止状态时结束活动。 要: 在成功、取消、退出登录、不可恢复的错误以及服务端终止性故障时结束活动。未结束的活动会让用户感到困扰。

不要: 假设每台设备都有灵动岛。 要: 将锁定屏幕作为主要显示界面进行设计;灵动岛是辅助显示界面。

不要: 在 ActivityAttributes 中存储敏感信息(这些信息会显示在锁定屏幕上)。 要: 将敏感数据保留在应用中,只显示可安全展示的摘要。

不要: 忘记处理过期日期。 要: 在视图中检查 context.isStale,并显示备用界面(例如“正在更新……”)。

不要: 忽略推送令牌轮换。令牌随时可能变化。 要: 使用 activity.pushTokenUpdates 异步序列,并在每次收到新值时重新注册。

不要: 忘记添加 NSSupportsLiveActivities Info.plist 键。 要: 将 NSSupportsLiveActivities = YES 添加到宿主应用的 Info.plist(而不是扩展)。

不要: 在请求/更新/结束时使用已弃用的基于 contentState 的 API。 要: 在所有生命周期调用中使用 ActivityContent。

不要: 直接从实时活动视图获取网络数据或位置信息。 要: 在应用或服务端预先计算显示值,并通过 ActivityKit 更新或推送传递这些值。

审查清单

  • ActivityAttributes 定义了静态属性和 ContentState
  • 宿主应用的 Info.plist 中设置了 NSSupportsLiveActivities = YES
  • Activity 使用 ActivityContent(而非已弃用的 contentState API)
  • 在所有终止路径(成功、错误、取消、退出登录、服务端终止性故障)中结束 Activity
  • 将 ActivityKit 生命周期以及锁定屏幕/灵动岛上的实时活动界面,与普通主屏幕/时间线小组件的工作区分开
  • 锁定屏幕布局(实时活动的主要界面)处理了 context.isStale
  • 实现了灵动岛的紧凑、展开和最小模式,并提供锁定屏幕回退界面
  • 通过 activity.pushTokenUpdates 将推送更新令牌转发给服务端
  • 通过 Activity<Attributes>.pushToStartTokenUpdates 收集推送启动令牌
  • 推送启动载荷包含必需的 alert
  • content-state JSON 与实际的 ContentState Codable 结构相匹配,包括协调一致的日期/范围编码
  • 审查时区分 8 小时的活动时长、系统结束后在锁定屏幕上总计显示 12 小时,以及应用结束后 .default 模式的 4 小时保留时间
  • 启动前检查 ActivityAuthorizationInfo
  • 检查 frequentPushesEnabled,再判断是否支持高频推送
  • 保持 ContentState 简洁(每次更新都会对其序列化)
  • 对 iOS 18+ 的 style:、.channel 和补充系列进行可用性检查
  • 合理选择 iOS 18+ 的 style::持久型实时活动使用 .standard,仅在灵动岛展开界面短暂显示时使用 .transient
  • 分别处理 ActivityKit 推送优先级和 aps.alert 行为
  • 实时活动视图避免直接执行网络/定位操作
  • 在设备上测试推送送达情况和灵动岛行为

参考资料

评论

支持 Markdown