十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

iOS灵动岛开发实战:Live Activity与ActivityKit完整接入指南

iOS灵动岛开发实战:Live Activity与ActivityKit完整接入指南 灵动岛从 iPhone 14 Pro 发布到现在已经不是一个新概念但对开发者来说它仍然是一个“看起来容易、做起来容易踩坑”的功能。很多人只是把灵动岛理解为挖孔屏上的黑色药丸动画实际上它是基于系统级 UI 渲染、Live Activity 状态管理和动态交互的一套完整能力。换句话说灵动岛的开发价值不在于把按钮塞进那个“岛”里而在于怎么用 Live Activities 把后台任务实时地呈现给用户并且在一套系统规范下安全、稳定地更新。这篇文章会把灵动岛拆成开发向的问题来看它能做什么、做不了什么、需要哪些前置条件、用 SwiftUI 怎么搭、用 ActivityKit 怎么创建和更新 Live Activity、真机调试要注意什么、常见崩溃和显示异常怎么排查。如果你正在做外卖进度、骑行导航、运动记录、会议提醒、音乐播放这类需要“锁屏也能看进度”的 App这篇文章可以直接当成一篇落地参考来用。1. 灵动岛核心能力速览动岛是 iOS 在系统层面提供的一种交互式状态展示区域。它的常用载体是 Live Activity实时活动开发者通过 ActivityKit 和 WidgetKit 来驱动显示内容。能力项说明系统要求支持 Live Activity 的系统版本通常为 iOS 16.1 及以上灵动岛显示以带灵动岛的机型为准开发语言Swift、SwiftUI部分场景需要 Widget Extension主要框架ActivityKit、WidgetKit、SwiftUI远程更新场景需要 APNs启动方式在 App 中调用 ActivityKit 请求启动 Live Activity灵动岛区域由系统接管核心功能锁屏实时活动、灵动岛紧凑显示、灵动岛扩展显示、状态更新、结束时删除交互能力点击灵动岛区域可唤起 App也可配合 Deep Link 跳转指定页面推送更新支持通过远程推送更新动态岛内容也支持本地刷新是否支持批量任务单个 App 可同时存在多个 Live Activity系统按顺序展示在灵动岛和锁屏上适合场景外卖配送、打车、运动计步、航班动态、计时器、下载进度、赛事比分、会议提醒等2. 适用场景与使用边界灵动岛适合展示的是“用户离开 App 之后仍然关心的短时任务”。典型特征是任务有明确的状态变化、变化频率不需要太高、用户希望在不解锁或不清扫的情况下就能看到结果。比较典型的场景包括外卖或快递订单的配送进度。打车时查看司机位置和预计到达时间。运动 App 记录跑步距离和时长。航班 App 展示登机口和起飞时间变化。音乐播放器在切到后台后显示播放状态。倒计时和番茄钟场景。灵动岛不适合做长驻通知也不适合做高频刷新的信息流。它本质上是“实时活动”的展示壳层苹果对 Live Activity 有明确的系统级限制比如自动过期时间、更新频率限制、展示位置策略等。把它当成普通通知中心来用很快就会遇到任务被系统自动清理或灵动岛区域被多次活动挤占的问题。这里也要提醒一句不要用灵动岛强制引导用户点击广告或诱导开启 Live Activity。展示内容如果涉及订单、定位、运动数据或用户隐私必须在合法授权的前提下使用。骑手位置、司机信息、用户行程这类数据单独看一个字段可能没问题但组合起来能推断出用户行为轨迹发布前要做隐私合规评估。涉及真实人物照片、声音、人脸识别等相关功能时上线前必须确认已经取得明确授权。3. 开发环境与前置条件3.1 硬件与系统前提灵动岛是硬件和系统配合的结果不是所有 iPhone 都能显示。做开发时至少需要一台带灵动岛的 iPhone 真机因为模拟器不能完全验证灵动岛在真实亮度、锁屏状态、通知共存下的表现。如果你要在真机上调试 Live Activity需要把系统升级到 iOS 16.1 及以上。Xcode 也要升级到支持 ActivityKit 的版本过于旧的 Xcode 连 framework 都找不到。3.2 Xcode 开发配置开发灵动岛功能会在一个普通 iOS App 工程里做这些事使用 ActivityKit 发起 Live Activity。新建一个 Widget Extension用来渲染锁屏和灵动岛。在 Widget Extension 中配置 ActivityConfiguration。在主 App 中根据业务事件调用更新和结束方法。创建项目和 Extension 时可以按以下路径操作File - New - Target选择Widget Extension勾选Include Live Activity填写 Extension 名称如果项目已经建好但没勾选 Include Live Activity也可以通过手动创建 ActivityConfiguration 的方式来补但更稳妥的做法是重新生成一个带 Live Activity 的 Widget Target然后再把代码迁进去。Widget Extension 需要独立的 Bundle Identifier通常是在主 App 的 Bundle ID 后面加.Widget。签名、授权、Team ID 配置好之后App 和 Widget Extension 才能共享 ActivityKit 的数据。3.3 部署目标建议由于 ActivityKit 是 iOS 16.1 以后才有的能力开发时不要直接把 Widget 的 deployment target 设到更低版本否则需要在代码里做可用性判断if #available(iOS 16.1, *) { // 使用 ActivityKit } else { // 降级到普通通知或本地推送 }这是很关键的一点。灵动岛能力只对支持 Live Activity 的系统版本生效老系统用户需要走原来的通知逻辑不能把灵动岛当成唯一的状态出口。4. 灵动岛 UI 设计与 SwiftUI 布局灵动岛并不是一块自由绘制的屏幕区域。苹果把它分成几类形态开发者的控制范围取决于当前状态。4.1 灵动岛的三种主要形态形态使用时机可布置内容紧凑形态只有一个 Live Activity 且不是最前时左侧图标或文字 右侧图标或文字最小形态多个 Live Activity 并存时单一小图标或极简信息扩展形态长按或处于前台时展开左侧、右侧、底部区域可放更多内容这种系统接管的设计决定你不应该用自定义 View 把整个岛盖住。做 UI 时应该先遵守系统给的区域划分和布局约束否则在部分机型上会出现截断、偏移或内容显示不全的问题。4.2 SwiftUI 基础布局灵动岛区域通常通过DynamicIslandExpandedRegion来组织内容。下面是一段典型的 SwiftUI 布局示意DynamicIsland { DynamicIslandExpandedRegion(.leading) { Label(配送中, systemImage: shippingbox.fill) .font(.headline) } DynamicIslandExpandedRegion(.trailing) { Text(剩余 800 米) .font(.caption) } DynamicIslandExpandedRegion(.bottom) { ProgressView(value: 0.8) .progressViewStyle(.linear) .tint(.green) Text(骑手正在配送请保持电话畅通) .font(.caption2) .foregroundColor(.secondary) } } compactLeading: { Image(systemName: shippingbox.fill) } compactTrailing: { Text(800m) .font(.caption2) } minimal: { Image(systemName: shippingbox.fill) }上面这段代码是在构建订单配送场景的灵动岛扩展区域。注意SwiftUI 布局不能超过安全区域边界信息过多时宁可精简也不要为了“显示完整”而使用会被裁剪的复杂布局。做灵动岛 UI 时一个实际经验是先做锁屏实时活动视图再做灵动岛视图。因为锁屏视图代码比较直接灵动岛区域则依赖系统状态切换调试起来成本更高。5. ActivityKit 接入与 Live Activity 开发UI 只是外观真正控制灵动岛生命周期的是 ActivityKit。5.1 定义 ActivityAttributes每个 Live Activity 需要在工程中定义一个遵循ActivityAttributes协议的类型。这个类型里要区分两类数据固定数据启动 Live Activity 之后不变化的属性。ContentState任务过程中会不断变化的状态数据。比如订单配送import ActivityKit struct DeliveryActivityAttributes: ActivityAttributes { public struct ContentState: Codable, Hashable { var statusText: String var progress: Double } var orderNumber: String var deliveryAddress: String }orderNumber适合放在外部固定数据中因为这一单启动后不会变。配送文案、进度条数值则要放进ContentState方便每次更新活动时替换。5.2 启动 Live Activity当用户下单成功或进入某个实时任务时主 App 调用 ActivityKit 启动活动。在 iOS 16.2 之前常见写法是let initialState DeliveryActivityAttributes.ContentState( statusText: 商家已接单, progress: 0.1 ) let activity try? ActivityDeliveryActivityAttributes.request( attributes: DeliveryActivityAttributes( orderNumber: A10086, deliveryAddress: 杭州市余杭区 ), contentState: initialState, pushType: nil )在 iOS 16.2 以后建议使用ActivityContentlet initialContent ActivityContent( state: DeliveryActivityAttributes.ContentState( statusText: 骑手已取餐, progress: 0.6 ), staleDate: nil ) let activity try? ActivityDeliveryActivityAttributes.request( attributes: DeliveryActivityAttributes( orderNumber: A10086, deliveryAddress: 杭州市余杭区 ), content: initialContent, pushType: nil )启动成功后系统会返回一个Activity实例。后面所有更新和结束操作都基于这个实例完成。注意启动 Live Activity 并不是一定成功。系统在磁盘空间不足、后台活动过多、状态受限等情况下可能拒绝创建。代码里不要用try!应该对失败做降级处理比如退回本地通知。5.3 更新 Live Activity当业务状态发生变化时主 App 要创建新的ContentState并调用更新。以骑手到达为例let newContent ActivityContent( state: DeliveryActivityAttributes.ContentState( statusText: 骑手已送达, progress: 1.0 ), staleDate: nil ) Task { await activity?.update(newContent) }实时状态更新不要太频繁。灵动岛面向的是短时任务不需要在几百毫秒内连续刷新几十次。频繁更新会造成电量消耗上升也容易触发系统对 Live Activity 的刷新限制。如果 App 在前台状态变化可以用本地 API 更新如果 App 退到后台甚至被杀死要依赖推送来唤醒系统更新。5.4 结束 Live Activity任务一旦到达终止状态就应该及时调用结束方法避免占用系统资源let finalContent ActivityContent( state: DeliveryActivityAttributes.ContentState( statusText: 订单已完成, progress: 1.0 ), staleDate: nil ) Task { await activity?.end(finalContent, dismissalPolicy: .immediate) }结束策略有三种常用情况终止方式适用场景.default由系统决定何时移除.immediate任务已经完成立即移除.after(Date)延迟一段时间后再移除适合展示完成后的奖励或总结不要忘记结束逻辑。尤其是外卖、打车、倒计时这类最终会终止的任务如果用户已经取消订单但 Live Activity 还挂在岛上体验会非常错乱。6. Widget Extension 中渲染灵动岛ActivityKit 只是控制生命周期真正画出来的是 Widget Extension。因为灵动岛会被系统在多个区域、多种形态下调度所有 UI 必须收敛到同一个ActivityConfiguration中。6.1 注册 Live Activity Widget在 Widget Bundle 中需要把锁屏 Widget 和 Live Activity Widget 都放进去main struct DeliveryWidgetBundle: WidgetBundle { var body: some Widget { DeliveryLockScreenWidget() DeliveryLiveActivity() } } struct DeliveryLiveActivity: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: DeliveryActivityAttributes.self) { context in // 这里是锁屏上的实时活动视图 LockScreenDeliveryView(context: context) } dynamicIsland: { context in // 这里是灵动岛视图 } } }这里的ActivityConfiguration是整个开发的桥接点。context.state对应ContentStatecontext.attributes对应自定义的固定属性。6.2 锁屏实时活动视图锁屏视图是一块相对宽裕的展示区域可以放置更多信息struct LockScreenDeliveryView: View { let context: ActivityViewContextDeliveryActivityAttributes var body: some View { HStack(spacing: 12) { Image(systemName: shippingbox.fill) .font(.title2) .foregroundColor(.blue) VStack(alignment: .leading, spacing: 4) { Text(订单 \(context.attributes.orderNumber)) .font(.headline) Text(context.state.statusText) .font(.subheadline) .foregroundColor(.secondary) ProgressView(value: context.state.progress) .tint(.blue) } } .padding() } }锁屏视图和普通 Widget 一样会被系统缓存不要在里面发起网络请求或执行耗时操作。它只是状态的“投影”不是业务逻辑执行器。6.3 让灵动岛能点击跳转灵动岛不是纯展示点击它可以唤起 App。这一步要做的是在动态岛区域里添加 SwiftUI 的Link或Button并通过 deep link 让 App 打开对应页面。DynamicIslandExpandedRegion(.bottom) { HStack { Text(context.state.statusText) Spacer() Link(destination: URL(string: yourapp://order/\(context.attributes.orderNumber))!) { Text(查看详情) } } }主 App 侧需要用onOpenURL处理这个 deep link根据订单号跳转到对应的详情页。如果 App 被灵动岛唤起时进程已经被系统清理就要在冷启动路由中解析 URL保证用户点进去后能看到正确页面。7. 远程推送更新与动态内容下发在实际业务中App 进程不一定常驻。订单状态往往由服务端更新这时候不能依赖 App 自己刷新 Live Activity必须通过推送更新灵动岛内容。远程更新 Live Activity 会用到 APNs 推送的content-state数据。服务端发送推送时请求体里包含活动对应的push-type、activity-id和状态字段。不同业务字段需要与 App 里的ContentState字段保持一致否则系统可能解析失败或展示旧数据。从开发流程上看需要先请求pushTokenTask { let pushToken try await activity.pushToken // 把 pushToken 上传到服务端 }请求到 token 之后由服务端保存并关联到当前订单。后面每个订单状态节点服务端都往 APNs 发一条 Live Activity 推送。推送内容不需要携带全部 UI 数据只需携带变化的ContentState字段。这套机制能把实时性从“App 还活着”扩展到“App 被杀死之后依然能更新”。远程推送更新需要后端配合所以做灵动岛功能时建议和后端把推流协议先对齐不要把字段定义放在客户端开发最后阶段才确认。8. 功能测试与效果验证灵动岛功能必须在真机上验证不能只看 SwiftUI 预览。建议按下面的顺序做测试8.1 基础启动测试先在 App 里点击一个按钮启动 Live Activity确认控制台没有报错随后按电源键锁屏观察灵动岛上是否出现紧凑形态。如果屏幕上同时存在多个 Live Activity长按灵动岛区域观察是否能展开并看到自己 App 的内容。8.2 状态更新测试启动 Live Activity 后切到后台再通过通知或本地模拟按钮触发状态更新。每更新一次锁屏和灵动岛上的文字、进度条都应该变化。若内容不变先检查是不是同一个 Activity 实例更新。一个常见错误是每次状态变化都调用request重新创建 Activity结果旧的 Activity 一直不结束导致灵动岛被多个重复活动占满。正常业务里应该把activity实例做成订单维度的单例或由管理器持有。8.3 结束测试结束 Activity 后锁屏和灵动岛上的内容都应消失。如果结束后仍然残留很可能是结束的不是同一个实例或者结束方法没有走完。测试时可以用这个顺序创建 Activity。写入日志记录 activity.id。用这个 id 执行更新和结束。检查 UI 是否按预期消失。8.4 推送测试推送更新是最容易出问题的环节。测试时不要直接从 App 内部调用更新而要模拟服务端推送从 APNs 发一条 payload 过来观察 UI 是否正常变化。一些推送服务会阻塞在证书或 token 校验阶段排错时先检查 APNs 响应码不要只看客户端是否报错。9. 资源占用与性能观察灵动岛日常承载的 UI 非常轻量它不是无限动画的画布。系统对 Live Activity 的更新频率、运行时长度、展示数据量都有限制这也是为了控制耗电和系统资源占用。在开发时观察性能和稳定性可以重点关注这几点大量动画是否卡顿。灵动岛不是动画播放器尽量避免使用复杂的 Spring 动画。高频更新是否被系统丢弃。如果业务要求每秒钟刷新多次位置建议先降到 5 到 10 秒一次再观察效果。多个 Activity 并存时是否出现覆盖。测试时至少要创建两个 Activity确认系统如何排列和切换。App 被杀死后Activity 是否还能正常更新。这个场景依赖推送链路本地更新无法覆盖。如果想分析耗电可以在真机上用 Xcode 的 Energy Log 或位置跟踪记录对比有 Live Activity 和无 Live Activity 时的耗电差异。但需要注意这跟推送频率、定位权限、屏幕常亮都有关系不能单看灵动岛一个变量。9.1 降低资源占用的常见做法禁用不必要的动态效果。减少Text和图片资源的频繁变化。同一时间尽量只保留一个 Live Activity。状态已经结束时就立即调用结束方法。推送频率控制在业务可容忍的最低水平。10. 常见问题与排查方法开发灵动岛功能时最经常遇到的是下面这些问题。问题现象可能原因排查方式解决方案Activity.request 无法创建活动系统版本低于 iOS 16.1或系统资源受限检查版本、检查磁盘可用空间和后台活动数量降级到 iOS 16.1 以下的通知重启设备后再试灵动岛区域不显示自己的 AppWidget Target 没有包含 Live Activity 配置检查 Widget Bundle 中是否注册 ActivityConfiguration注册对应 ActivityConfiguration动态内容更新后 UI 没变化更新了旧的 Activity 实例对比控制台打印的 activity.id更新正确的活动实例必要时统一管理 Activity更新后 UI 延迟或丢失更新频率过高或被系统节流检查业务侧是否每几百毫秒触发一次更新降低更新频率合并多次中间状态多个活动同时存在时被挤掉单 App 或系统同时活动数量限制创建多个 Activity 观察切换优先保留最关键的实时活动及时结束不重要的活动锁屏正常但灵动岛空白Widget 中没有实现 dynamicIsland 闭包检查 ActivityConfiguration 的 dynamicIsland 部分代码补充 compactLeading、compactTrailing、minimal 实现推送更新失败pushToken 未上传、payload 字段不一致或 APNs 证书错误检查服务端 APNs 返回值按 APNs 文档修正 payload 和签名模拟器无法完整验证模拟器没有真实硬件形态和环境换真机测试所有最终验证必须在支持灵动岛的 iPhone 真机上执行此外许多在 SwiftUI 预览中正常显示的布局放到真机后可能被截断。排查时先把文本数量、字体大小、自定义 padding 都降到系统默认状态确认是不是自建布局超出安全区域导致的问题。11. 最佳实践与使用建议如果团队第一次接灵动岛建议按这套流程来做先做锁屏实时活动再做灵动岛。这样能把 ActivityKit 生命周期和 UI 渲染两件事拆开减少一次调多个变量的排查成本。一个 Activity 对应一个有明确 id 的业务实体不要散落在任意 View 里。创建、更新、结束三组方法统一封装成服务类方便在业务层调用也方便打日志。所有 ContentState 字段需要设计成可 Codable 的稳定结构因为推送更新和服务端共用同一套 JSON 字段。在线状态变化时把中间态压缩为最终态。比如配送过程里骑手位置连续变化了几十次推到 Live Activity 上时只保留最近几次关键状态即可。测试推送更新时从 APNs 返回的失败信息开始排查而不是反复在客户端里尝试。上线前要检查深链接是否在冷启动、热启动、后台恢复三种状态下都能正确跳转。涉及订单、地理位置、用户身份等数据时确认推送内容里不包含不必要的敏感字段。即使推送内容需要展示地址信息也最好在前端截取展示字段不要把原始完整地址传给临时推送链路。发布前留出真机回归时间灵动岛最终效果依赖真实硬件自动化测试很难完全覆盖。灵动岛这项技术本身不复杂难点在于和业务生命周期耦合。只要 Live Activity 的启动时机、状态更新频率、结束条件这三个点设计得足够清楚开发过程就不会太痛苦。接下来的重点可以继续放在远程推送链路和业务稳定性上先把一个高频场景跑通再逐步扩展到更多任务类型。
返回列表