首先,我们先简单了解下灵动岛
Live Activities 依赖于 Widget 实现 函数和页面,而与Widget不同,Live Activities无法访问网络或接收位置更新,更新Live Activities可以使用ActivityKit和远程推送,同时ActivityKit可以控制Live Activities的开始,更新和结束。
灵动岛一共有三种样式展示:
-
只有一个Live Activities活动时,如下图,将在灵动岛的左右两个部分显示信息(紧凑级),点击打开App查看详细信息
-
而同时有多个Live Activities活动时,系统最多展示只两个(最小级)Live Activities活动,一个将紧贴灵动岛,一个单独展示在圆圈内,如下图:
-
手指按中其中任何一个,系统将展示(拓展视图),如下图:
灵动岛拓展区域划分见下图:
下面是手把手实现灵动岛功能
1. 创建Live Activities,如下图步骤
2. 完成创建Live Activities后,在主项目的Info.plist中添加NSSupportsLiveActivities = YES。
3. 下一步,根据[ActivityAttributes] 结构代码定义Live Activities所需的静态和动态数据,如图,在主项目创建:
4. 根据刚才创建的[ActivityAttributes]来创建灵动岛页面,如下代码:
import ActivityKit
import WidgetKit
import SwiftUI
//灵动岛界面配置
struct UavLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: UavAttributes.self) { context in
// 创建显示在锁定屏幕上的演示,并在不支持动态岛的设备的主屏幕上作为横幅。
HStack(alignment: .center) {
Image("uav-logo")
.frame(width: 53,height: 59)
if context.state.driverStatus == "1"{
VStack(alignment:.leading){
Text("物流备货中")
.font(.title3)
.fontWeight(.bold)
Text("请耐心等待…")
.font(.callout)
.foregroundColor(Color(red: 102/255, green: 102/255, blue: 102/255))
}.padding(.leading,20)
Spacer()
Image("uav-wait")
.frame(width: 30,height: 30)
}else if context.state.driverStatus == "2"{
VStack(alignment:.leading){
Text("已备货,等待配送")
.font(.title3)
.fontWeight(.bold)
Text("请耐心等待…")
.font(.callout)
.foregroundColor(Color(red: 102/255, green: 102/255, blue: 102/255))
}.padding(.leading,20)
Spacer()
Image("uav-wait")
.frame(width: 30,height: 30)
}else if context.state.driverStatus == "3"{
VStack(alignment: .leading) {
HStack{
Text("飞手配送中")
.font(.title3)
.fontWeight(.bold)
Spacer()
Text("距离目的地")
.font(.title3)
+
Text(context.state.distance)
.font(.title3)
+
Text("m")
.font(.title3)
}
HStack(alignment: .center){
let prog = context.state.progress/100
GeometryReader { geometry in
Image("uav-line")
.resizable()
.offset(x:0, y:1)
.frame(width: geometry.size.width , height:4)
RoundedRectangle(cornerRadius: 15)
.fill(.blue)
.padding(.trailing, geometry.size.width*(CGFloat(1 - prog)))
Image("uav-icon")
.resizable()
.frame(width:30, height:30)
.padding(.leading,geometry.size.width*CGFloat(prog))
.offset(x: -8, y:-12)
}
.frame(height: 6)
Image("uav-ship")
.resizable()
.frame(width:30, height:30)
}
}
}else if context.state.driverStatus == "4"{
VStack(alignment:.leading){
Text("商品已送达")
.font(.title3)
.fontWeight(.bold)
Text("欢迎使用,祝你生活愉快!")
.font(.callout)
.foregroundColor(Color(red: 102/255, green: 102/255, blue: 102/255))
}.padding(.leading,20)
Spacer()
Image("uav-complete")
.frame(width: 30,height: 30)
}
}
.padding(20)
.widgetURL(URL(string: "cjh-my"))
} dynamicIsland: { context in
// 创建显示在动态岛中的内容。
DynamicIsland {
//这里创建拓展内容(长按灵动岛)
DynamicIslandExpandedRegion(.leading) {
if context.state.driverStatus == "1"{
VStack{
Image("uav-smlogo")
Text("物流备货中")
}.padding(.leading,10)
}else if context.state.driverStatus == "2"{
VStack{
Image("uav-smlogo")
Text("已备货,等待配送")
}.padding(.leading,10)
}else if context.state.driverStatus == "3"{
VStack{
Image("uav-smlogo")
Text("飞手配送中")
}.padding(.leading,10)
}else{
VStack{
Image("uav-smlogo")
Text("商品已送达")
}.padding(.leading,10)
}
}
DynamicIslandExpandedRegion(.trailing) {
if context.state.driverStatus == "1"{
VStack{
Image("uav-smwait")
Spacer()
Text("请耐心等待...")
}.padding(.trailing,10)
}else if context.state.driverStatus == "2"{
VStack{
Image("uav-smwait")
Spacer()
Text("请耐心等待...")
}.padding(.trailing,10)
}else if context.state.driverStatus == "3"{
VStack{
Image("uav-smicon")
Spacer()
Text("距离")
.font(.title3)
+
Text(context.state.distance)
.font(.title3)
+
Text("m")
.font(.title3)
}.padding(.trailing,10)
}else{
VStack{
Image("uav-smcomplete")
Spacer()
Text("祝您生活愉快")
}.padding(.trailing,10)
}
}
DynamicIslandExpandedRegion(.center) {
}
DynamicIslandExpandedRegion(.bottom) {
}
}
//下面是紧凑展示内容区(只展示一个时的视图)
compactLeading: {
Image("uav-smlogo")
.resizable()
.frame(height:25)
.padding(.leading,10)
} compactTrailing: {
if context.state.driverStatus == "1"{
Image("uav-smwait")
.padding(.trailing,10)
}else if context.state.driverStatus == "2"{
Image("uav-smwait")
.padding(.trailing,10)
}else if context.state.driverStatus == "3"{
Image("uav-smicon")
.padding(.trailing,10)
}else{
Image("uav-smcomplete")
.padding(.trailing,10)
}
}
//当多个Live Activities处于活动时,展示此处极小视图
minimal: {
VStack(alignment: .center) {
Image(systemName: "timer")
}
}
.keylineTint(.accentColor)
.widgetURL(URL(string: "cjh-my"))
}
}
}
灵动岛页面需要实现的部分有4个
不支持灵动岛的机型 或 锁屏时的 显示
紧凑级展示(即左右贴合灵动岛的展示)
多Live activity时的展示(即极小视图,左贴合,右分离)
拓展视图(长按时触发)
5. 完成上面步骤,Live activity 已经创建好了,下面是启动:
(下面代码包含和极光推送交互的代码,通过激光推送更新数据,把liveactivityId发送极光推送)
import UIKit
Import ActivityKit
import SwiftUI
class UavManage: NSObject {
@available(iOS 16.1,*)
@objc func startDeliveryAction(liveactivityId:String) {
//初始化静态数据
let uavAttributes = UavAttributes()
//初始化动态数据
let initialContentState = UavAttributes.UavStatus(distance: "10", driverStatus: "3", progress: 50)
do {
//启用灵动岛
//灵动岛只支持Iphone,areActivitiesEnabled用来判断设备是否支持,即便是不支持的设备,依旧可以提供不支持的样式展示
if ActivityAuthorizationInfo().areActivities Enabled == true{
}
let deliveryActivity = try Activity<UavAttributes>.request(
attributes: uavAttributes,
contentState: initialContentState,
pushType: PushType.token)
Task {
// 监听 push token 更新
to await pushToken in deliveryActivity.pushTokenUpdates {
let pushtokenstr = pushToken.map { String(format: "%02.2hhx", arguments: [$0]) }.joined()
// 向极光注册liveactivity,在推送平台通过liveactivityId推送通知进行更新
JPUSHService.registerLiveActivity(liveactivityId, pushToken: pushToken, completion: { code, liveactivityId, token, seq in
print("registerLiveActivity liveactivityId:\(String(describing: liveactivityId)) token: \(pushtokenstr) result:\(code) seq:\(seq)")
}, seq: 1)
let paraDic:[String:String] = ["token":pushtokenstr,"alias":liveactivityId]
HttpUtil.post(withUrl: "activeUavsDelivery", withReq:paraDic) { succeedResult in
if succeedResult?["code"] as! String == "0000" {
}
} failedBlock: { task, error in
}
}
}
} catch (let error) {
print("Error info -> \(error.localizedDescription)")
}
}
}
灵动岛的活动状态一共有3种:
- active 处于活动中
- ended 已经终止且不会有任何更新,但依旧在锁屏界面展示
- dismissed 结束且不再展示
6. 更新灵动岛数据(更新的就是在ActivityAttributes中声明的动态数据):
下面的图片我是用别人的,本项目通过消息推送更新数据的
func updateDeliveryPizza(){
Task {
let updatedDeliveryStatus =
PizzaDeliveryAttributes.PizzaDeliveryStatus (driverName:"快递小哥" deliveryTimer: Date()... Date() .addingTimeInterval (60 * 60))
//此处只有一个灵动岛,当一个项目有多个灵动岛时,需要判断更新对应的activity
for activity in Activity<PizzaDeliveryAttributes>.activitiest{
await activity.update(using: updatedDeliveryStatus)
}
}
}
Live activity也支持远程推送更新,根据文档以下9点要求实现(Activity远程通知每小时有通知预算<数量未明确>,超出后系统将关闭通知)
1. 确保主程序已经开通了远程推送功能
2. 确保启动activity时[request(attributes:contentState:pushType:)传入pushType参数(.token)
3. 获取启动后的activity的推送令牌pushToken,传给服务端用来推送更新activity
4. 服务端推送的更新内容字段需要和ActivityAttributes的ContentState 中定义的动态数据字段对应
5. 设置推送的报头apns-push-type的值为liveactivity
6. 设置推送的报头apns-topic的值为<your bundleID>.push-type.liveactivity
7. 正确的推送对应的内容和状态
8. 使用pushTokenUpdates监听pushToken变化,如有变化,就令牌失效,需要将新的令牌传给服务器
9. 当Activity结束时,服务器端的pushToken将失效
下面是官方提供的示例:
{
"aps": {
"timestamp": 1168364460,
"event": "update",
"content-state": {
"driverName": "Anne Johnson",
"estimatedDeliveryTime": 1659416400
},
"alert": {
"title": "Delivery Update",
"body": "Your pizza order will arrive soon.",
"sound": "example.aiff"
}
}
}
可以在aps内设置dissall -date 字段来告诉系统在什么时候移除activity ,eg: “dismissal-date”: 1663177260
不用为推送提供声音 , 如果推送延迟,在activity结束后收到时将被忽略,avtivity每小时有通知预算(数量未明确),超出后系统将关闭通知
7. 结束Activity:
func stopDeliveryPizza () {
Task {
for activity in Activity<PizzaDeliveryAttributes>.activitiest{
await activitv. end(dismissalPolicv: . immediate)
}
}
}
结束分为两种:
.default 系统默认,结束后在锁屏界面保留4小时
.immediate 立即结束,不会在锁屏界面停留
下面是通过极光推送实现的方式,实时活动数据的更新和结束
- 消息内容:添加的是动态数据,结束和更新相比会多一个实时活动结束的时间;
- 目标群体:是之前通过代码给极光推送注册的liveactivityId;
- 下面的图是手动推送的方式,后面主要通过后台和极光推送交互进行推送;
- 有个注意点是模拟器不会产生临时令牌,最终还是通过真机进行测试。
参考文档:https://www.jianshu.com/p/10541a43010c
官方文档:https://developer.apple.com/documentation/activitykit/starting-and-updating-live-activities-with-activitykit-push-notifications#Construct-the-ActivityKit-push-notification-payload
参考代码:https://github.com/jiajun1203/LiveActivities?tab=readme-ov-file