实时活动(Live Activities)是iOS 16引入的系统级特性,它允许应用将短暂、动态的信息固定在锁屏或灵动岛上,非常适合展示比赛比分、外卖进度、航班动态等时效性强的数据。开发实时活动需要结合WidgetKit和ActivityKit两个框架:WidgetKit负责渲染视图,ActivityKit负责管理活动的启动、更新与结束。整个过程并不复杂,但涉及扩展进程和主App进程之间的数据传递,如果对状态机没有清晰认识,很容易在配置阶段卡壳。

实时活动的工作原理与适用场景
实时活动在系统层面被设计为一种“短暂状态”的展示载体。它不像普通Widget那样通过时间线刷新快照,而是由ActivityKit维护一个活动的生命周期:应用发起请求后,系统会为这个活动分配一个唯一的标识,并在锁屏或灵动岛上生成对应视图;应用可以多次更新该活动的状态数据,系统会立即刷新展示内容;当活动结束时,视图会以一定的策略(如立即消失或保留一段时间)从屏幕上移除。整个过程中,主App不需要在前台运行,甚至进程被挂起也不会影响活动更新。
从适用场景来看,凡是需要持续跟踪进度、但又有明确终点的信息,都适合用实时活动展示。例如篮球比赛的实时比分、外卖骑手的配送进度、一场直播的剩余时间,甚至是健身记录的卡路里消耗。这类信息的特点是更新频率不高,但又需要用户在不解锁手机的情况下扫一眼就能掌握动态。此外,实时活动也可以配合灵动岛使用,让用户在iPhone 14 Pro系列机型上获得更沉浸的交互体验。
与普通Widget相比,实时活动的状态是有“生命周期”的,而不是无限期展示。普通Widget在任何时刻都能通过时间线生成内容,而实时活动必须在应用内通过ActivityKit显式开启、更新和结束。系统会优先保证实时活动的资源占用,但也限制同时存在的活动数量,避免滥用。理解这种差异,是后续开发中选择正确API的前提。
环境准备与项目配置
要开发实时活动,首先需要一个支持WidgetKit的工程。在Xcode中,选择File菜单下的New > Target,然后选择“Widget Extension”。在模板配置界面,有一个“Include Live Activities”的复选框,勾选后Xcode会自动创建一个包含实时活动配置的Widget组。如果你的工程已经有Widget Extension,也可以通过手动添加WidgetKit和ActivityKit框架的方式来支持。
除了创建Target,还要在主App的Info.plist中增加一个关键配置项,否则Activity.kit会拒绝启动任何实时活动。这个配置是NSSupportsLiveActivities,必须设置为布尔值YES。如果还需要通过远程推送来更新活动,可以再添加NSSupportsLiveActivitiesFrequentUpdates键,设置为YES以允许高频更新。下面是一个典型的Info.plist片段:
<key>NSSupportsLiveActivities</key> <true/> <key>NSSupportsLiveActivitiesFrequentUpdates</key> <true/>
这段配置的目的是告诉系统,本应用有实时活动的能力,并且支持频繁刷新。如果只做本地更新,可以省略第二个键;但如果你打算用推送远程驱动活动,那么第二个键也建议开启。需要注意的是,Info.plist中的键名是固定的,拼写错误不会导致编译失败,但运行时会静默失效,很多开发者忽略这一点,导致活动无法启动。
通过ActivityKit管理活动生命周期
ActivityKit是实时活动的核心框架,它要求开发者定义两个关键类型:Attributes和ContentState。Attributes描述活动的静态信息,例如比赛双方队伍名称、外卖订单号;ContentState描述活动的动态状态,例如双方当前比分、骑手剩余距离。两者都必须遵循Codable和Hashable协议。一个典型的定义如下:
import ActivityKit
import WidgetKit
struct GameAttributes: ActivityAttributes {
public struct ContentState: Codable, Hashable {
var homeScore: Int
var awayScore: Int
var status: String
}
var homeTeam: String
var awayTeam: String
}
有了Attributes类型后,就可以在主App中启动一个实时活动。整个过程是异步的,因为系统需要为活动分配资源。你需要在调用时传入Attributes实例、初始ContentState,以及一个可选的推送类型。推送类型传nil表示仅支持本地更新,传.push表示支持远程推送更新。下面是一个启动活动的示例:
let attributes = GameAttributes(homeTeam: "湖人", awayTeam: "凯尔特人")
let initialContent = ActivityContent(
state: GameAttributes.ContentState(homeScore: 0, awayScore: 0, status: "准备中"),
staleDate: nil
)
do {
let activity = try Activity.request(
attributes: attributes,
content: initialContent,
pushType: nil
)
print("活动已启动:\(activity.id)")
} catch {
print("启动活动失败:\(error)")
}
启动成功后会返回一个Activity实例,该实例包含一个唯一的id。你可以保留这个实例,用于后续更新和结束。更新时,只需要创建一个新的ContentState,然后调用update(using:)方法。结束活动时可以传入最终状态,并指定dismissalPolicy,例如在活动结束后立即移除,或者保持一段时间。示例代码如下:
Task {
let newState = GameAttributes.ContentState(homeScore: 2, awayScore: 1, status: "进行中")
await activity.update(using: newState)
}
Task {
let finalState = GameAttributes.ContentState(homeScore: 3, awayScore: 1, status: "已结束")
await activity.end(using: finalState, dismissalPolicy: .immediate)
}
值得注意的是,ActivityKit的API在iOS 16.0和16.2之间做了一次调整。早期版本使用request(attributes:contentState:pushType:),之后版本引入了ActivityContent包装器。上面的示例使用了较新的方式,适用于iOS 16.2及以上系统。如果你要兼容更早的iOS 16版本,需要针对系统版本做条件编译。
设计锁屏与灵动岛界面
实时活动的UI由Widget Extension中的ActivityConfiguration来声明。这个配置接受一个Attributes类型作为泛型参数,并分别提供锁屏视图和灵动岛视图的构建块。在锁屏视图的闭包中,系统会传入一个ActivityViewContext,它包含了Attributes和最新的ContentState。你可以使用SwiftUI的ViewBuilder自由设计布局。
下面是一个锁屏界面的简单实现,它展示了队名、比分和比赛状态。由于锁屏空间有限,建议使用大号字体和简洁的排版,确保用户一瞥就能获取关键信息。
struct LockScreenView: View {
let context: ActivityViewContext<GameAttributes>
var body: some View {
VStack {
HStack {
Text(context.attributes.homeTeam)
Spacer()
Text("\(context.state.homeScore) : \(context.state.awayScore)")
.font(.title2)
.fontWeight(.bold)
Spacer()
Text(context.attributes.awayTeam)
}
Text(context.state.status)
.font(.footnote)
.foregroundColor(.secondary)
}
.padding()
}
}
注意到在Swift代码中,ActivityViewContext<GameAttributes>的写法需要把尖括号转义,因为它是Swift泛型的一部分,但在HTML中直接写<GameAttributes>可以确保正确显示。如果你使用的是Xcode提供的模板,它通常会生成一个完整的GameLiveActivity结构体,并在其中同时提供锁屏和Dynamic Island的UI。Dynamic Island的构建块包含多个区域,例如展开区域的leading、trailing、center,以及紧凑模式的leading、trailing和minimal视图。以下是一个支持灵动岛的完整示例:
struct GameLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: GameAttributes.self) { context in
LockScreenView(context: context)
} dynamicIsland: { context in
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
Text(context.attributes.homeTeam)
.font(.headline)
}
DynamicIslandExpandedRegion(.trailing) {
Text(context.attributes.awayTeam)
.font(.headline)
}
DynamicIslandExpandedRegion(.center) {
HStack {
Text("\(context.state.homeScore)")
Text(":")
Text("\(context.state.awayScore)")
}
.font(.title)
}
} compactLeading: {
Text("\(context.state.homeScore)")
} compactTrailing: {
Text("\(context.state.awayScore)")
} minimal: {
Text("VS")
}
}
}
}
在Dynamic Island的设计中,系统会限制不同区域可以使用的控件类型和空间尺寸。展开区域可以放更多信息,但也要避免拥挤。紧凑和最小区域只能放少量信息,因此通常只显示数字或极简图标。实时活动的UI不能包含滚动视图、动画循环等交互组件,它本质上仍是静态视图的量化更新,过度复杂的设计反而会被系统压缩。
调试与常见问题
实时活动的调试在模拟器和真机上的体验差别很大。模拟器从iOS 16.2开始支持实时活动,但灵动岛只能在支持灵动岛的真机上模拟。在模拟器中,你可以通过Xcode的“Debug”菜单下的“Synchronize Widgets”来强制刷新,但并不能触发真实的推送更新路径。因此,建议在真机上验证端到端流程,尤其是推送更新场景。
另一个常见问题是活动数量限制。系统会限制每个App同时存在的实时活动数量,超出限制时新的请求会抛出异常。如果活动长期不更新,系统也会自动将其回收,释放资源。因此,在设计时需要为活动设置一个合理的过期时间或结束策略,不要无限期持有。更新频率也需要注意,即使开启了频繁更新权限,也不会允许以每秒多次的频率刷新,系统会默认对高频更新进行节流。
如果你计划通过远程推送更新实时活动,还需要在Widget Extension中实现ActivityConfiguration的承载推送方法,并在你的后端服务中集成Apple的推送服务(APNs)。推送内容需要按照ActivityKit规定的payload格式构造,否则活动不会刷新。常见的坑包括:忘记在Info.plist中开启NSSupportsLiveActivities,导致Activity.request直接失败;或者在Widget和主App之间共享数据时忘记使用App Group,导致ActivityKit无法获取正确的状态。开发时可以先从本地更新入手,跑通生命周期后再接入推送,这样排查起来会更清晰。
实时活动虽然技术门槛不高,但它的设计思路和普通Widget大相径庭。你需要明确活动的开始、更新和结束时机,并为不同尺寸的锁屏和灵动岛适配不同布局。通过本文提供的配置和代码,你应该能够快速搭建一个比赛比分展示的实时活动,并轻松迁移到外卖进度等其他场景。记得在真机上多测试不同机型,确保各种尺寸下都能有良好的可读性和稳定性。
WidgetKitLive Activities锁屏实时活动修改时间:2026-08-28 14:52:46