[阅读难度:简单]
准备工作
技术储备:
需对Xcode、Swift、SwiftUI 有一定了解
环境要求:
iOS >= 16.1
Xcode >= 14.1
设备(iPhone14Pro/iPhone14ProMax,模拟器也可以)
灵动岛简介
一句话总结:“灵动岛是展示在锁屏界面与主屏幕状态栏的“特殊”小组件”
灵动岛的状态:
默认状态:可展示左右两个小视图(
LeadingSide
、TrailingSide
)
展开状态:用户长按灵动岛的展开状态(Leading
、Center
、Trailing
、Bottom
),
紧凑状态:当有多个APP灵动岛时展示分离状态,依创建顺序展示(MinimalDetached
)
灵动岛的生命周期:
灵动岛主要分为
Start
、Update
、End
三种状态,可由ActivityKit
与远程推送
控制其状态。
一个灵动岛默认展示8
小时,结束后可继续在锁屏界面存在4
个小时后由系统彻底移除,也可由开发者自行移除。
那么,我们开始吧·····
接入Widget Extension
info.plist 文件中添加
NSSupportsLiveActivities
字段,设置为YES
-
选中项目添加target,创建
Widget Extension
,假设将其命名为“DynamicDemo”:
在Widget中,主要关注
WidgetsExtensionBundle
,该结构体下的body返回所有小组件的Widget,Xcode 会默认生成小组件卡片的初始代码,不需要的话可以移除。
@main
struct WidgetsExtensionBundle: WidgetBundle {
var body: some Widget {
// DynamicDemo() //原本的小组件卡片Widget
DynamicDemoLiveActivity() // 灵动岛Widget
}
}
Tips:模拟器下偶现APP动态小组件权限被关闭,从“设置-对应APP-实时活动”打开即可
2. 配置灵动岛Widget
在Xcode14.1
及之后的版本引入 WidgetsExtension
后,会默认创建XXXXXXLiveActivity.swift
文件:
struct DynamicDemoAttributes: ActivityAttributes {
public struct ContentState: Codable, Hashable {
// Dynamic stateful properties about your activity go here!
var value: Int
}
// Fixed non-changing properties about your activity go here!
var name: String
}
struct DynamicDemoLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: DynamicDemoAttributes.self) { context in
// Lock screen/banner UI goes here
VStack {
Text("Hello")
}
.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")
}
DynamicIslandExpandedRegion(.trailing) {
Text("Trailing")
}
DynamicIslandExpandedRegion(.bottom) {
Text("Bottom")
// more content
}
} compactLeading: {
Text("L")
} compactTrailing: {
Text("T")
} minimal: {
Text("Min")
}
.widgetURL(URL(string: "http://www.apple.com"))
.keylineTint(Color.red)
}
}
}
DynamicDemoLiveActivity
: 灵动岛的Widget,通过ActivityConfiguration
来配置所有状态下的视图,包括不支持灵动岛设备的设备在锁屏页面的视图。
DynamicDemoAttributes
: 灵动岛属性属性结构体,用于数据更新,由对应的Activity使用。
3. 控制灵动岛
在需要控制灵动岛的地方,引入ActivityKit
。针对“Start”、“Update”、“End”三种状态分别举个简单的例子:
开启:
Task{
// 配置Attributes
let data = DynamicDemoAttributes(value: 100)
let state = DynamicDemoAttributes.ContentState()
// 根据 Attributes 开启一个灵动岛
do {
// reqeust 指定 pushType 为 .token 可获取远程推送所需的 token
try await MainActor.run {
let activity = try Activity<DynamicDemoAttributes>.request(attributes: data, contentState: state)
}
} catch (let error) {
print(error.localizedDescription)
}
}
更新:
do {
let data = DynamicDemoAttributes(value:100)
let state = DynamicDemoAttributes.ContentState(progressMsg: "更新的文案")
if let activity = Activity<DynamicDemoAttributes>.activities.first {
await activity.update(using: state)
} else {
try Activity<DynamicDemoAttributes>.request(attributes: data, contentState: state)
}
} catch (let error) {
print(error.localizedDescription)
}
代码中取的 activities.first
,实际开发中应该根据数据来区分并获取需要更新的灵动岛实例。
通过获取当前需要更新的灵动岛实例,通过调用update方法传入最新的Attributes更新。
Tips: 灵动岛可由宿主APP与远程推送更新,普通APP会在退到后台后10s内被挂起,例如音乐、健身、定位、通话等可常驻后台的应用可直接通过宿主APP及时更新。对于退出APP后需要更新的场景,可由远程推送实现。具体可参考文末。
结束:
//移除所有
Activity<DynamicDemoAttributes>.activities.forEach { item in
Task{
// await item.end() //默认结束后,会在锁屏界面等待4小时彻底移除
await item.end(dismissalPolicy:.immediate) // 立即结束
}
}
Tips: 指定
.immediate
可以立即移除
Q&A
Q1:如何识别用户点击的视图:
当前的灵动岛不支持非系统自定义交互(截止20221121),支持DeepLink
、Link
跳转。点击灵动岛默认跳转我们的宿主APP,我们可以在灵动岛视图中配置widgetURL()
、Link()
来达到识别用户点击的效果、以Link()
举例:
···············
DynamicIslandExpandedRegion(.bottom) {
Link(destination: URL(string: "xxxxxx://hotel/contact")!) {
Label("Contact Hotel", systemImage: "phone").padding()
}.background(Color.accentColor)
.clipShape(RoundedRectangle(cornerRadius: 15))
}
···············
上面代码可以在用户点击了灵动岛的bottom视图跳转进APP时在 AppDelegate
或 SceneDelegate
方法中识别其所带入的 url
,以此来做对应处理。
Q2:多个灵动岛展示的优先级
- 单个APP的多个灵动岛:默认视图(依次展示)
- 多个APP的灵动岛:紧凑视图/分离视图(依次展示)
Q3:如何在模拟器模拟远程推送更新
常规的推送可查看《iOS 在模拟器上测试远程推送》。在调研灵动岛的过程中,需要模拟器测试远程推送的功能。查阅一圈资料后成功实现,具体步骤如下:
环境要求:
Xcode >= 14.1
,
MacOS >= 13.0
准备工作
如何获取远程推送的token:
let activity = try
// 指定pushType为.token
Activity<OrderProgressWidgetAttributes>.request(attributes: data, contentState: state, pushType: .token)
orderProgressActivity = activity
Task {
for await data in activity.pushTokenUpdates {
let myToken = data.map {String(format: "%02x", $0)}.joined()
print(myToken);
// 将token告知后端用于远程推送
}
}
运行CommandLine
配置所需要使用的宏:
$ export TEAM_ID={TEAM ID}
$ export TOKEN_KEY_FILE_NAME={Token Key file path}
$ export AUTH_KEY_ID={your Auth Key ID}
$ export DEVICE_TOKEN={myToken from the activity push token} 设备Token(灵动岛是Activity start后返回的pushtoken)
$ export APNS_HOST_NAME=api.sandbox.push.apple.com
说明:
TEAM_ID:开发者TeamID(开发者账户可查)
TOKEN_KEY_FILE_NAME:开发者账户生成的Keys下载后的文件路径(.p8格式)
AUTH_KEY_ID:开发者账户生成的keys的ID(开发者账号内可查、下载的.p8文件默认后缀前的十个字符)
DEVICE_TOKEN:设备Token(灵动岛场景下是Activity指定pushType为.token后start返回的pushtoken)
配置命令行推送(命令行逐行运行,注意中英字符):
$ 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:po.test.push-type.liveactivity" \
--header "apns-push-type:liveactivity" \
--header "authorization:bearer $AUTHENTICATION_TOKEN" \
--data \
'{"Simulator Target Bundle": "po.test",
"aps": {
"timestamp":1668764100,
"event": "update",
"content-state": {
"progressMsg": "酒店订单已确认",
}
}}' \
--http2 \
https://${APNS_HOST_NAME}/3/device/$DEVICE_TOKEN
apns-topic
:固定为{BundleId}.push-type.liveactivity
apns-push-type
:固定为liveactivity
Simulator Target Bundle
:测试模拟器推送,设置为对应应用的{BundleId}
timestamp
:刷新时间戳。需设置正确,否则灵动岛的推送不会生效
event
:可填入update、end,对应灵动岛的更新与结束。
content-state
:对应灵动岛的Attributes
参考资料
本文只是简单介绍了基础功能的实现,更丰富的探索可查阅官方文档:
https://developer.apple.com/documentation/activitykit/displaying-live-data-with-live-activities
https://developer.apple.com/documentation/widgetkit/dynamicisland/
https://developer.apple.com/documentation/activitykit/update-and-end-your-live-activity-with-remote-push-notifications
https://developer.apple.com/design/human-interface-guidelines/components/system-experiences/live-activities
https://developer.apple.com/news/?id=mis6swzt