Live Activity(实时活动)是 iOS 16 引入的一项强大功能,用于在锁屏和灵动岛上展示实时更新的信息。与普通 Widget 不同,Live Activity 支持秒级更新、支持远程推送触发更新,是外卖配送、赛事比分、计时器等时效性场景的理想选择。
本篇将带你从零开始构建一个完整的 Live Activity。
1. Live Activity 与传统 Widget 的区别
| 维度 | 传统 Widget | Live Activity |
|---|---|---|
| 框架 | WidgetKit | ActivityKit + WidgetKit(视图部分) |
| 更新频率 | 分钟级(最低约 15 分钟) | 秒级 |
| 触发方式 | Timeline 自动调度 | App 主动推送 / APNs 远程推送 |
| 生命周期 | 由系统管理 | 由 App 显式启动和结束 |
| 展示位置 | 主屏、锁屏、待机等 | 灵动岛、锁屏横幅 |
| 数据流向 | 被动拉取(Timeline) | 主动推送(update) |
| 是否有状态 | 无状态(每次重新构建 Timeline) | 有状态(activityState:active/ended) |
2. 创建 Live Activity
Step 1:添加 Live Activity Target
创建 Widget Extension 时勾选 Include Live Activity,或在已有 Target 中添加 Live Activity 文件。
Step 2:定义 ActivityAttributes
swift
import ActivityKit
struct DeliveryAttributes: ActivityAttributes {
// 静态数据(活动创建时确定,不可变)
public struct ContentState: Codable, Hashable {
// 动态数据(可更新)
var statusText: String // "商家正在备货"
var progress: Double // 0.0 ~ 1.0
var estimatedMinutes: Int // 预计剩余分钟数
var driverName: String?
var driverPhoto: String?
}
// 静态属性
var orderNumber: String
var restaurantName: String
var totalAmount: String
}
Step 3:构建灵动岛视图
swift
import WidgetKit
import SwiftUI
struct DeliveryActivityWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: DeliveryAttributes.self) { context in
// 锁屏展示
VStack(alignment: .leading, spacing: 8) {
HStack {
Image(systemName: "takeoutbag.and.cup.and.straw.fill")
.font(.title)
VStack(alignment: .leading) {
Text(context.attributes.restaurantName)
.font(.headline)
Text("订单号: \(context.attributes.orderNumber)")
.font(.caption)
.foregroundColor(.secondary)
}
}
ProgressView(value: context.state.progress) {
Text(context.state.statusText)
.font(.caption)
}
HStack {
Label("预计 \(context.state.estimatedMinutes) 分钟", systemImage: "clock")
.font(.caption)
.foregroundColor(.secondary)
Spacer()
if let driverName = context.state.driverName {
Label(driverName, systemImage: "person.circle")
.font(.caption)
.foregroundColor(.secondary)
}
}
}
.padding()
.activityBackgroundTint(Color.black.opacity(0.2))
.activitySystemActionForegroundColor(.white)
} dynamicIsland: { context in
// 灵动岛展示
DynamicIsland {
// 展开区域
DynamicIslandExpandedRegion(.leading) {
Label(context.state.statusText, systemImage: "takeoutbag.and.cup.and.straw.fill")
.font(.caption)
}
DynamicIslandExpandedRegion(.trailing) {
Label("\(context.state.estimatedMinutes)分钟", systemImage: "clock")
.font(.caption)
}
DynamicIslandExpandedRegion(.bottom) {
ProgressView(value: context.state.progress)
.tint(.green)
}
} compactLeading: {
// 灵动岛左侧紧凑视图
Image(systemName: "takeoutbag.and.cup.and.straw.fill")
.font(.caption)
} compactTrailing: {
// 灵动岛右侧紧凑视图
Text("\(context.state.estimatedMinutes)min")
.font(.caption2)
} minimal: {
// 极小模式
Image(systemName: "takeoutbag.and.cup.and.straw.fill")
}
}
}
}
3. 启动与管理 Live Activity
启动 Activity
swift
import ActivityKit
func startDeliveryActivity(order: Order) throws {
let attributes = DeliveryAttributes(
orderNumber: order.number,
restaurantName: order.restaurantName,
totalAmount: order.totalAmount
)
let contentState = DeliveryAttributes.ContentState(
statusText: "商家已接单",
progress: 0.1,
estimatedMinutes: 30,
driverName: nil,
driverPhoto: nil
)
let activity = try Activity<DeliveryAttributes>.request(
attributes: attributes,
contentState: contentState,
pushType: .token // 如果需要远程推送更新,设置 pushType
)
// 保存 push token(用于远程推送更新)
Task {
for await pushToken in activity.pushTokenUpdates {
let tokenString = pushToken.map { String(format: "%02x", $0) }.joined()
// 发送 token 到你的服务器
await sendTokenToServer(tokenString)
}
}
}
更新 Activity
swift
func updateDeliveryStatus(activity: Activity<DeliveryAttributes>, newStatus: DeliveryAttributes.ContentState) async {
await activity.update(using: newStatus)
}
结束 Activity
swift
func endDelivery(activity: Activity<DeliveryAttributes>) async {
let finalState = DeliveryAttributes.ContentState(
statusText: "已送达",
progress: 1.0,
estimatedMinutes: 0,
driverName: "张师傅",
driverPhoto: nil
)
await activity.end(using: finalState, dismissalPolicy: .after(Date().addingTimeInterval(3600)))
// dismissalPolicy: .default(.after(x)) 在 x 时间后自动移除
// .immediate 立即移除
}
4. 通过 APNs 远程推送更新
Live Activity 最大的优势之一是支持通过 APNs 远程推送更新,适合服务器驱动的实时场景。
推送 Payload 格式
json
{
"aps": {
"timestamp": 1680000000,
"event": "update",
"content-state": {
"statusText": "骑手正在配送中",
"progress": 0.5,
"estimatedMinutes": 15,
"driverName": "李师傅"
}
}
}
注意事项
- 推送 token 与普通推送 token 不同,需通过
pushTokenUpdates获取 - 推送频率受系统限制,建议不要过于频繁(APNs 会对高频推送进行节流)
content-state必须与ContentState结构完全匹配
5. 监听 Activity 状态变化
swift
// 监听所有活跃的 Activity
func monitorActivities() {
Task {
for await activity in Activity<DeliveryAttributes>.activityUpdates {
print("Activity 状态: \(activity.activityState)")
}
}
}
// 监听特定 Activity 的状态
func monitorActivity(_ activity: Activity<DeliveryAttributes>) {
Task {
for await state in activity.activityStateUpdates {
switch state {
case .active:
print("Activity 活跃中")
case .ended:
print("Activity 已结束")
case .dismissed:
print("Activity 已被移除")
@unknown default:
break
}
}
}
}
6. 完整实战:倒计时 Live Activity
swift
// Attributes
struct TimerAttributes: ActivityAttributes {
public struct ContentState: Codable, Hashable {
var targetDate: Date
var label: String
}
var timerName: String
}
// 启动倒计时
func startTimerActivity(name: String, targetDate: Date) throws {
let attributes = TimerAttributes(timerName: name)
let state = TimerAttributes.ContentState(targetDate: targetDate, label: name)
let activity = try Activity<TimerAttributes>.request(
attributes: attributes,
contentState: state,
pushType: nil
)
}
// Widget 视图(锁屏展示倒计时)
@main
struct TimerActivityWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: TimerAttributes.self) { context in
VStack(spacing: 4) {
Text(context.attributes.timerName)
.font(.headline)
Text(context.state.targetDate, style: .timer)
.font(.system(size: 48, weight: .bold, design: .monospaced))
.multilineTextAlignment(.center)
}
.padding()
} dynamicIsland: { context in
DynamicIsland {
DynamicIslandExpandedRegion(.center) {
Text(context.state.targetDate, style: .timer)
.font(.title)
}
} compactLeading: {
Image(systemName: "timer")
} compactTrailing: {
Text(context.state.targetDate, style: .timer)
.font(.caption2)
} minimal: {
Image(systemName: "timer")
}
}
}
}
7. 限制与注意事项
| 限制 | 说明 |
|---|---|
| 最大活跃数 | 每个 App 通常限制为 5 个活跃 Activity |
| 数据大小 | ContentState 需要尽量小(避免大 JSON) |
| 远程推送更新频率 | 过高频率会被 APNs 节流 |
| Activity 持续时间 | 最长 8 小时(结束后可继续展示 4 小时) |
| 需用户授权 | Live Activity 需要在系统设置中授权才能展示 |
| 不支持交互按钮 | 灵动岛展开视图不支持 Button(intent:) |
小结
- Live Activity 适合需要频繁更新的时效性信息展示
- 通过
ActivityAttributes+ContentState定义数据结构 - 支持本地更新和 APNs 远程推送更新
- 灵动岛有三种展示模式:Compact、Expanded、Minimal
- 注意推送频率限制和活跃数量限制
最后,希望这篇文章能帮到有需要的朋友,如果觉得有帮助,点个赞、加个关注,笔者也会继续努力输出更多优质内容。
上一篇 :iOS Widget 开发-12:Widget 深度链接与导航
下一篇 :iOS Widget 开发-14:iOS 18 控制中心组件开发