导读:本期聚焦于小白龙创作的《如何使用WidgetKit开发Live Activities在锁屏展示比赛比分或外卖进度?》,敬请观看详情。实时活动(Live Activities)并不是普通的Widget,它依赖ActivityKit框架在锁屏和灵动岛上展现动态状态。本文从底层原理入手,讲解如何通过Xcode创建支持实时活动的Widget Extension,配置Info.plist关键选项,定义Attributes与ContentState数据结构,然后使用Activity请求启动、更新和结束活动。针对锁屏和灵动岛区域,还会说明如何利用ActivityFamily区分不同的UI布局。调试部分则会介绍模拟器与真机的差异,以及推送更新时的注意事项,帮助开发者避开常见的坑。无论你想实现比赛比分还是外卖进度,这篇指南都可以快速带你搭建出可运行的实时活动功能。

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

如何使用WidgetKit开发Live Activities在锁屏展示比赛比分或外卖进度?

实时活动的工作原理与适用场景

实时活动在系统层面被设计为一种“短暂状态”的展示载体。它不像普通Widget那样通过时间线刷新快照,而是由ActivityKit维护一个活动的生命周期:应用发起请求后,系统会为这个活动分配一个唯一的标识,并在锁屏或灵动岛上生成对应视图;应用可以多次更新该活动的状态数据,系统会立即刷新展示内容;当活动结束时,视图会以一定的策略(如立即消失或保留一段时间)从屏幕上移除。整个过程中,主App不需要在前台运行,甚至进程被挂起也不会影响活动更新。

从适用场景来看,凡是需要持续跟踪进度、但又有明确终点的信息,都适合用实时活动展示。例如篮球比赛的实时比分、外卖骑手的配送进度、一场直播的剩余时间,甚至是健身记录的卡路里消耗。这类信息的特点是更新频率不高,但又需要用户在不解锁手机的情况下扫一眼就能掌握动态。此外,实时活动也可以配合灵动岛使用,让用户在iPhone 14 Pro系列机型上获得更沉浸的交互体验。

与普通Widget相比,实时活动的状态是有“生命周期”的,而不是无限期展示。普通Widget在任何时刻都能通过时间线生成内容,而实时活动必须在应用内通过ActivityKit显式开启、更新和结束。系统会优先保证实时活动的资源占用,但也限制同时存在的活动数量,避免滥用。理解这种差异,是后续开发中选择正确API的前提。

环境准备与项目配置

要开发实时活动,首先需要一个支持WidgetKit的工程。在Xcode中,选择File菜单下的New > Target,然后选择“Widget Extension”。在模板配置界面,有一个“Include Live Activities”的复选框,勾选后Xcode会自动创建一个包含实时活动配置的Widget组。如果你的工程已经有Widget Extension,也可以通过手动添加WidgetKitActivityKit框架的方式来支持。

除了创建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描述活动的动态状态,例如双方当前比分、骑手剩余距离。两者都必须遵循CodableHashable协议。一个典型的定义如下:

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

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。