实现 LiveActivity(灵动岛)

限制说明

iOS 16.1 及以上版本支持。
灵动上岛最多保留八小时,在锁定屏幕上最多保留十二小时。
远端操作需要用户配置 p8 证书。
设备只支持 iPhone,并且是有“药丸屏”的 iPhone14Pro 和 14Pro Max 上。

接入指引

步骤1:增加 Live Activities 支持配置

需要在主程序的 Info.plist 中添加键值:Supports Live Activities 为 YES。

步骤2:创建 WidgetExtension,如果项目中已经存在,则跳过该步骤。




步骤3:代码实现

1.实现 Activity Attributes

以下实现需要按照自己的业务实现对应的数据模型,其中 ContentState 里为可以动态更新的数据,activityID 建议定义为 LiveActivity 的唯一标识 ID。
import Foundation
import ActivityKit

struct LiveActivityAttributes: ActivityAttributes {
public struct ContentState: Codable, Hashable {
// Dynamic stateful properties about your activity go here!
var text: String
var pauseTime: Date?
var endTime: Date?
}

// Fixed non-changing properties about your activity go here!
// custom activityID when creating a LiveActivity
var activityID: String
}

2.创建 UI

具体 UI 请按照自己的业务编写,包含了锁屏 UI 和灵动岛 UI 的定义:
import ActivityKit
import WidgetKit
import SwiftUI

struct LiveActivityLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: LiveActivityAttributes.self) { context in
// Lock screen/banner UI goes here
VStack {
Text("Hello \(context.state.text)")
}
.activityBackgroundTint(Color.cyan)
.activitySystemActionForegroundColor(Color.black)

} dynamicIsland: { context in
DynamicIsland {
// Expanded UI goes here. Compose the expanded UI through
// various regions, like leading/trailing/center/bottom
DynamicIslandExpandedRegion(.leading) {
Text("Leading \(context.attributes.activityID)\(context.state.text)")
}
DynamicIslandExpandedRegion(.trailing) {
Text("Trailing \(context.state.text)")
}
DynamicIslandExpandedRegion(.center) {
Text("Center \(context.state.text)")
}
DynamicIslandExpandedRegion(.bottom) {
Text("Bottom \(context.state.text)")
// more content
}
} compactLeading: {
Text("CL \(context.state.text)")
} compactTrailing: {
Text("CT \(context.state.text)")
} minimal: {
Text("CB \(context.state.text)")
}
.widgetURL(URL(string: "https://cloud.tencent.com/document/product/269/100621"))
.keylineTint(Color.red)
}
}
}
灵动岛 UI 布局定义如下:


3.客户端操作,添加启动、更新和停止逻辑

// start
let activity = try Activity.request(
attributes: adventure,
content: .init(state: initialState, staleDate: nil),
pushType: .token
)
// update
await activity.update(
ActivityContent<AdventureAttributes.ContentState>(
state: contentState,
staleDate: Date.now + 15,
relevanceScore: alert ? 100 : 50
),
alertConfiguration: alertConfig
)
// end
await activity.end(ActivityContent(state: finalContent, staleDate: nil), dismissalPolicy: dismissalPolicy)

4.远端上报配置和更新操作

监听 token 更新和上报
Task {
for await pushToken in activity.pushTokenUpdates {
let pushTokenString = pushToken.hexadecimalString
Logger().debug("New push token: \(pushTokenString)")
try await self.setLiveActivity(activityID:activity.attributes.activityID, pushToken: pushToken)
}
}


func setLiveActivity(activityID: String, pushToken: Data) async throws {
var _apnsConfig = ImSDK_Plus.V2TIMLiveActivityConfig()
_apnsConfig.businessID = xxxx
_apnsConfig.token = pushToken
_apnsConfig.activityID = activityID
os_log("%@", type: .debug, "setLiveActivity activityID: \(activityID)\ntoken:\(pushToken.hexadecimalString)")
ImSDK_Plus.V2TIMManager.sharedInstance().setLiveActivity(_apnsConfig, succ: {
print("setLiveActivity succ")
}, fail: {code, desc in
print("setLiveActivity fail, \(code), \(desc)")
})
}
上报清理 LiveActivity
func clearActivity(activityID: String) async throws {
os_log("clearActivity ID: \(activityID)")
ImSDK_Plus.V2TIMManager.sharedInstance().setLiveActivity(nil, succ: {
print("clearActivity succ")
}, fail: {code, desc in
print("clearActivity fail, \(code), \(desc)")
})
}
通过服务端更新和停止 iOS 实时活动。
注意:
1. LiveActivity 推送限制 p8 证书,必须配置 P8 证书。
2. 如果接收方没有上报对应 activityID,将自动降级为普通通知推送。
ApnsInfo.LiveActivity 对应的 JSON Object格式说明
字段名称
类型
选项
字段说明
LaId
string
必填
需要推送的实时活动标识,对应客户端 activityID。
长度不超过64字节。
Event
string
必填
更新:“update”,结束:"end"。
ContentState
JSON Object
必填
自定义的 key:value 的 object。需与客户端 SDK 值匹配。
DismissalDate
Integer
选填
event 为 end 时,锁屏实时活动结束展示的 uinx 时间。
不填默认为当前时间,锁屏会马上结束。
请求示例:
示例中省略或未列出参数,请参考文档:单发单聊批发单聊offlinePushInfo 相关
{
"MsgBody": [...] // 这里同 MsgBody 相关描述
"OfflinePushInfo": {
"PushFlag": 0,
"Title": "离线推送标题",
"Desc": "离线推送内容",
"Ext": "{\"entity\":{\"k1\":\"v1\",\"k2\":\"v2\"}}", // 透传字段,推送使用json格式字符串
"ApnsInfo": {
"LiveActivity": {
"LaId": "timpush",
"Event": "update", // update LA
"ContentState": {
"k1": v1,
"k2": v2,
...
}
}
}
"AndroidInfo": {
... // AndroidInfo相关参考官网文档
}
}
}
{
"MsgBody": [...] // 这里同 MsgBody 相关描述
"OfflinePushInfo": {
"PushFlag": 0,
"Title": "离线推送标题",
"Desc": "离线推送内容",
"Ext": "{\"entity\":{\"k1\":\"v1\",\"k2\":\"v2\"}}", // 透传字段,推送使用json格式字符串
"ApnsInfo": {
"LiveActivity": {
"LaId": "timpush",
"Event": "end", // end LA
"ContentState": {
"k1": v1,
"k2": v2,
...
},
"DismissalDate": 1739502750
}
}
"AndroidInfo": {
... // AndroidInfo相关参考官网文档
}
}
}