mcpField
L
live-activities

live-activities

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

关于 live-activities

live-activities 是用于构建和审查 ActivityKit 体验的 Swift 开发技能,面向开发实时速览界面的开发者。内容涵盖活动生命周期、Dynamic Island 布局、推送更新,以及较新的定时和显示选项。

主要功能

  1. 01ActivityKit 设置与生命周期模式
  2. 02锁定屏幕和 Dynamic Island 布局
  3. 03APNs 推送更新与远程启动
  4. 04定时活动与基于频道的活动
  5. 05代码审查清单与常见问题

适用场景

  1. 01在锁定屏幕显示配送进度
  2. 02发送由服务器驱动的活动更新
  3. 03为未来事件安排活动
  4. 04审查 Live Activity 实现

安装

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

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

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

附带文件references/live-activity-patterns.md

详细介绍

实时活动与灵动岛

使用 ActivityKit,在锁定屏幕、灵动岛、待机模式、CarPlay 和 Mac 菜单栏中构建实时、一目了然的体验。设计模式面向 iOS 26+ 和 Swift 6.2,除非另有说明,否则向后兼容至 iOS 16.1。

有关完整代码模式(包括推送负载格式、并发活动、状态观察和测试),请参阅 references/live-activity-patterns.md。

目录

工作流

1. 创建新的实时活动
  1. 将 NSSupportsLiveActivities = YES 添加到宿主应用的 Info.plist。
  2. 定义一个带有嵌套 ContentState 的 ActivityAttributes 结构体。
  3. 在 Widget 包中创建 ActivityConfiguration,并提供锁定屏幕内容和灵动岛闭包。
  4. 使用 Activity.request(attributes:content:pushType:) 启动活动。
  5. 使用 activity.update(_:) 更新活动,并使用 activity.end(_:dismissalPolicy:) 结束活动。
  6. 将推送令牌转发到你的服务端,以支持远程更新。
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;在视图中显示备用界面(例如“正在更新…”)。

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

活动生命周期

启动

使用 Activity.request 创建并显示实时活动。将 .token 作为 pushType 传入,以启用通过 APNs 进行的远程更新。

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

灵动岛

iPhone 14 Pro 及后续机型支持灵动岛。它有三种呈现模式。请为所有模式进行设计,但应将锁定屏幕视为主要显示界面,因为并非所有设备都配备灵动岛。

紧凑模式(前导区域 + 尾随区域)

单个实时活动处于活跃状态时始终可见。可用空间极为有限——只显示最关键的信息。

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

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

展开的区域

用户长按灵动岛时显示。

区域位置
.leadingTrueDepth 摄像头左侧;向下延伸
.trailingTrueDepth 摄像头右侧;向下延伸
.center摄像头正下方
.bottom所有其他区域下方
边缘色调

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

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

推送更新

推送更新通过 APNs 发送实时活动更新,比从应用轮询更高效,并且在应用暂停时也能正常工作。

设置

启动活动时,将 .token 作为 pushType 传入,然后将推送令牌转发到你的服务端:

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 负载格式

向 APNs 发送带有以下请求头和 JSON 正文的 HTTP/2 POST 请求:

必需的 HTTP 请求头:

  • apns-push-type: liveactivity
  • apns-topic: <bundle-id>.push-type.liveactivity
  • apns-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 结构完全匹配。键或类型不匹配会导致静默失败。

推送启动

无需应用运行即可远程启动实时活动(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)
    }
}
高频推送更新

将 NSSupportsLiveActivitiesFrequentUpdates = YES 添加到 Info.plist,以提高推送更新额度。适用于每分钟更新多次的活动(体育比分、乘车追踪)。

iOS 26 新增功能

计划的实时活动(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,
    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+)

实时活动会自动显示在 macOS Tahoe 菜单栏(通过 iPhone 镜像)和 CarPlay 主屏幕中。无需额外代码——确保锁定屏幕布局在较小尺寸下仍清晰易读。

基于频道的推送(iOS 18+)

使用 .channel 同时向多个实时活动广播更新:

let activity = try Activity.request(
    attributes: attributes, content: content,
    pushType: .channel("delivery-updates")
)

常见错误

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

不要: 通过应用过于频繁地更新 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 与模拟器上的表现不同)
  • 确保 ActivityAttributes 和 ContentState 类型符合 Sendable;在 @MainActor 上更新 Live Activity UI

参考资料

  • 有关模式和代码示例,请参阅 references/live-activity-patterns.md

评论

支持 Markdown