1. 前言
实时活动是iOS 16.1及以上版本中新增的功能,它允许应用在锁屏界面显示实时数据,能够帮助用户实时查看当前订单的进展,而无需解锁手机。用户在货拉拉APP上下单后,可以将手机放置一旁,开始其他工作。当用户想要查询订单状态时,只需从锁定屏幕或灵动岛上轻松操作即可。实时活动的出现不仅省去了用户解锁手机的步骤,更为用户节省了时间和精力。目前货拉拉APP适配"灵动岛"的最新6.7.68版本已正式上线,欢迎大家升级体验。在适配过程中,货拉拉App也踩过很多"坑",在此汇总为实战经验分享给大家。
2. Live Activity&灵动岛的介绍
Live Activity的实现需要使用Apple的ActivityKit框架。通过使用ActivityKit,开发者可以轻松地创建一个Live Activity,这是一个动态的、实时更新的活动,可以在用户的设备上显示各种信息。此外,ActivityKit还提供了推送通知的功能,开发者可以通过服务器向用户的设备发送更新;这样,即使应用程序没有运行,用户也可以接收到最新的信息。
灵动岛是Live Activity的一种展示形式,灵动岛有三种展示形式:Compact紧凑、Minimal最小化,Expanded扩展。开发时必须实现这三种形式,以确保灵动岛在不同的场景下都能正常展示。
同时还需要实现锁屏下的实时活动UI,设备处于锁屏状态下,也能查看实时更新的内容。以上功能的实现,都是使用WidgetKit和SwiftUI完成开发。
2.1 技术难点及策略
实时活动,主要是APP在后台时,主动更新通知栏和灵动岛的数据,为用户展示最新实时订单状态。如何及时刷新实时活动的数据,是一个重点、难点。
更新方式有3种:
- 通过APP内订单状态的变化刷新实时活动和灵动岛。此方法开发量小,但是APP退到后台30s后或者进程杀掉,会停止数据的更新。
- 让APP配置支持后台运行模式,通过本地现有的订单状态变化逻辑,在后台发起网络请求,获取订单的数据后刷新实时活动。此方法开发量小,但求主App进程必须存在,进程一旦杀掉就无法更新。
- 通过接受远程推送通知来更新实时活动。此方法需要后端配合,此方式比较灵活,无需App进程存在,数据更新及时。也是业界常见的方案。
通过对数据刷新的三种方案进行评估后,选择了用户体验最佳的第三种方式。通过后端发生push,端上接受push数据来更新实时活动。
3. Live Activity&灵动岛的实践
3.1 实现方案流程图
实现流程图:
3.2 实现代码
创建Live Activities的准备:
- Xcode需要14.1以上版本
- 在主工程的 Info.plist 文件中添加一个键值对,key 为 NSSupportsLiveActivities,value 为 YES
- 使用ActivityKit在Widget Extension 中创建一个Live Activity
需要实现锁屏状态下UI、灵动岛长按展开的UI、灵动岛单个UI、多个实时活动时的minimalUI
swift
import SwiftUI
import WidgetKit
@main
struct TestWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: TestAttributes.self) { context in
// 锁屏状态下的UI
} dynamicIsland: { context in
DynamicIsland {
//灵动岛展开后的UI
} compactLeading: {
// 未被展开左边UI
} compactTrailing: {
// 未被展开右边UI
} minimal: {
// 多任务时,右边的一个圆圈区域
}
.keylineTint(.cyan)
}
}
}
灵动岛主要分为Start
、Update
、End
三种状态,可由ActivityKit
与远程推送
控制其状态。
开启Live Activity:
csharp
let state = TestAttributes.ContentState()
let attri = TestAttributes(value: 100)
do {
let current = try Activity.request(attributes: attri, contentState: state, pushType: .token)
Task {
for await state in current.contentStateUpdates {
//监听state状态
}
}
Task {
for await state in current.activityStateUpdates {
//监听activity状态
}
}
} catch(let error) {
}
更新Live Activity:
swift
Task {
guard let current = Activity<TestAttributes>.activities.first else {
return
}
let state = TestAttributes.ContentState(value: 88)
await current.update(using: state)
}
结束Live Activity:
rust
Task {
for activity in Activity<TestAttributes>.activities {
await activity.end(dismissalPolicy: .immediate)
}
}
4. 使用ActivityKit推送通知
ActivityKit提供了接收推送令牌的功能,我们可以使用这个令牌来通过ActivityKit推送通知从我们的服务器向Apple Push Notification service (APNs)发送更新。
推送更新Live Activity的准备:
-
在开发者后台配置生成p8证书,替换原来的p12证书
-
通过
pushTokenUpdates
获取推送令牌PushToken
-
向后端注册
PushToken
代码展示:
javascript
//取得PushToken
for await tokenData in current.pushTokenUpdates {
let mytoken = tokenData.map { String(format: "%02x", $0) }.joined()
//向后端注册
registerActivityToken(mytoken)
}
4.1 模拟器push验证测试
环境要求:
Xcode >= 14.1
MacOS >= 13.0
准备工作:
- 通过
pushTokenUpdates
获取推送需要的token - 根据开发者TeamID、p8证书本地路径、BuidleID等进行脚本配置
脚本示例:
bash
export TEAM_ID=YOUR_TEAM_ID
export TOKEN_KEY_FILE_NAME=YOUR_AUTHKEY_FILE.p8
export AUTH_KEY_ID=YOUR_AUTHKEY_ID
export DEVICE_TOKEN=YOUR_PUSH_TOKEN
export APNS_HOST_NAME=api.sandbox.push.apple.com
export JWT_ISSUE_TIME=$(date +%s)
export JWT_HEADER=$(printf '{ "alg": "ES256", "kid": "%s" }' "${AUTH_KEY_ID}" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
export JWT_CLAIMS=$(printf '{ "iss": "%s", "iat": %d }' "${TEAM_ID}" "${JWT_ISSUE_TIME}" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
export JWT_HEADER_CLAIMS="${JWT_HEADER}.${JWT_CLAIMS}"
export JWT_SIGNED_HEADER_CLAIMS=$(printf "${JWT_HEADER_CLAIMS}" | openssl dgst -binary -sha256 -sign "${TOKEN_KEY_FILE_NAME}" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
export AUTHENTICATION_TOKEN="${JWT_HEADER}.${JWT_CLAIMS}.${JWT_SIGNED_HEADER_CLAIMS}"
curl -v \
--header "apns-topic:YOUR_BUNDLE_ID.push-type.liveactivity" \
--header "apns-push-type:liveactivity" \
--header "authorization: bearer $AUTHENTICATION_TOKEN" \
--data \
'{"Simulator Target Bundle": "YOUR_BUNDLE_ID",
"aps": {
"timestamp":1689648272,
"dismissal-date":0,
"event": "update",
"sound":"default",
"content-state": {
"title": "等待付款",
"content": "请尽快完成下单"
}
}}' \
--http2 \
https://${APNS_HOST_NAME}/3/device/$DEVICE_TOKEN
其中:
apns-topic
:固定为{BundleId}.push-type.liveactivity
apns-push-type
:固定为liveactivity
Simulator Target Bundle
:模拟器推送,设置为对应APP的BundleId
timestamp
:表示推送通知的发送时间,如果timestamp
字段的值与当前时间相差太大,可能会收不到推送。
event
:可填入update、end,对应Live Activity的更新与结束。
dismissal-date
:当event为end时有效,表示结束后从锁屏上移除Live Activity的时间。如果推送内容不包含"dismissal-date",默认结束后4小时后消失,但内容不会再发生更新。如果期望Live Activity结束后立即从锁屏上移除它,可为"dismissal-date"提供一个过去的日期。
content-state
:对应灵动岛的Activity.ContentState
;如果push中content-state
的字段和Attributes比较:
-
字段过多,多余的字段可能会被忽略,不会导致解析失败
-
字段缺少,会在解析push通知时出现问题错误。错误表现为:实时活动会有蒙层,并展示loading菊花UI。
示范:
5. 踩坑记录
-
在模拟器上无法获取到pushToken,无法进行推送模拟?
检查电脑的系统版本号,需要13.0以上
-
更新实时活动时,页面显示加载loadingUI,为什么?
核对push字段和
Activity.ContentState
的字段是否完全一致,字段少了会解析失败 -
在16.1系统上,无法展示实时活动,其他更高系统能展示?
检查Widget里面iOS系统版本号的配置,设置为想要支持的最低版本
-
dismissal-date
设置为10分钟后才消失,为什么Dynamic Island灵动岛立即消失了?Dynamic Island的显示逻辑可能会更加复杂,如果push的event=end,Dynamic Island灵动岛会立即消失。期望同时消失,可以在指定时间再发end,
dismissal-date
设置为过去时间,锁屏UI和Dynamic Island灵动岛会同时消失。 -
推送不希望打扰用户,静默推送,不需要震动和主动弹出,如何设置?
将"content-available"设置为1,"sound" 设置为: ""
json
"aps" = {
"content-available" : 1,
"sound" : ""
}
-
用户系统是深色模式时,如何适配?
可以使用
@Environment(.colorScheme)
属性包装器来获取当前设备的颜色模式。会返回一个ColorScheme
枚举,它可以是.light
或.dark
。在根据具体的场景进行UI适配
scss
struct ContentView: View {
@Environment(.colorScheme) var colorScheme
var body: some View {
VStack {
if colorScheme == .dark {
Text("深夜模式")
.foregroundColor(.white)
.background(Color.black)
} else {
Text("日间模式")
.foregroundColor(.(.black)
.background(Color.white)
}
}
}
}
5.1 场景限制及建议
- 官方文档提示实时活动最多持续8小时,8小时后数据无法刷新,12小时后会强制消失。因此8小时后的数据不准确
- 实时活动的卡片上禁止定位以及网络请求,数据需要小于4KB,不能展示特别负责庞大的数据
- 同场景多卡片由于样式趋同且折叠,不建议同时创建多卡片。用户多次下单时,建议只处理第一个订单
6. 用户APP上线效果
用户端iOS APP灵动岛上线后的部分场景截图:
7. 总结
灵动岛功能自上线以来,经过我们的数据统计,用户实时活动使用率高达75%以上。这一数据的背后,是灵动岛强大的功能和优秀的用户体验。用户可以在锁屏页直接查看订单状态,无需繁琐的操作步骤,大大提升了用户体验。这种便捷性,使得灵动岛在用户中的接受度较高。
我们的方案不仅可以应用于当前的业务场景,后续还计划扩展到营销活动,定制化通知消息等多种业务场景。这种扩展性,使得灵动岛可以更好地满足不同用户的需求,丰富产品运营策略。
我们希望通过分享开发过程中遇到的问题和解决方案,可以帮助到更多的人。如果你有任何问题或者想法,欢迎在评论区留言。期待我们在技术的道路上再次相遇。
总的来说,灵动岛以其高效、便捷、灵活的特性,赢得了用户的广泛好评。我们将继续努力,为用户提供更优质的服务,为产品的发展注入更多的活力。