首先壮韭,我們先簡(jiǎn)單了解下靈動(dòng)島
Live Activities 依賴于 Widget 實(shí)現(xiàn) 函數(shù)和頁(yè)面挖腰,而與Widget不同,Live Activities無(wú)法訪問(wèn)網(wǎng)絡(luò)或接收位置更新,更新Live Activities可以使用ActivityKit和遠(yuǎn)程推送唉工,同時(shí)ActivityKit可以控制Live Activities的開(kāi)始求妹,更新和結(jié)束乏盐。
靈動(dòng)島一共有三種樣式展示:
-
只有一個(gè)Live Activities活動(dòng)時(shí),如下圖制恍,將在靈動(dòng)島的左右兩個(gè)部分顯示信息(緊湊級(jí))父能,點(diǎn)擊打開(kāi)App查看詳細(xì)信息
image -
而同時(shí)有多個(gè)Live Activities活動(dòng)時(shí),系統(tǒng)最多展示只兩個(gè)(最小級(jí))Live Activities活動(dòng)吧趣,一個(gè)將緊貼靈動(dòng)島法竞,一個(gè)單獨(dú)展示在圓圈內(nèi)耙厚,如下圖:
image.jpg -
手指按中其中任何一個(gè),系統(tǒng)將展示(拓展視圖)岔霸,如下圖:
image.jpeg
靈動(dòng)島拓展區(qū)域劃分見(jiàn)下圖:
image.jpeg
下面是手把手實(shí)現(xiàn)靈動(dòng)島功能
1. 創(chuàng)建Live Activities,如下圖步驟
2. 完成創(chuàng)建Live Activities后薛躬,在主項(xiàng)目的Info.plist中添加NSSupportsLiveActivities = YES。
3. 下一步呆细,根據(jù)[ActivityAttributes] 結(jié)構(gòu)代碼定義Live Activities所需的靜態(tài)和動(dòng)態(tài)數(shù)據(jù)型宝,如圖,在主項(xiàng)目創(chuàng)建:
4. 根據(jù)剛才創(chuàng)建的[ActivityAttributes]來(lái)創(chuàng)建靈動(dòng)島頁(yè)面絮爷,如下代碼:
import ActivityKit
import WidgetKit
import SwiftUI
//靈動(dòng)島界面配置
struct UavLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: UavAttributes.self) { context in
// 創(chuàng)建顯示在鎖定屏幕上的演示趴酣,并在不支持動(dòng)態(tài)島的設(shè)備的主屏幕上作為橫幅。
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("請(qǐng)耐心等待…")
.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("請(qǐng)耐心等待…")
.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("商品已送達(dá)")
.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
// 創(chuàng)建顯示在動(dòng)態(tài)島中的內(nèi)容。
DynamicIsland {
//這里創(chuàng)建拓展內(nèi)容(長(zhǎng)按靈動(dòng)島)
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("商品已送達(dá)")
}.padding(.leading,10)
}
}
DynamicIslandExpandedRegion(.trailing) {
if context.state.driverStatus == "1"{
VStack{
Image("uav-smwait")
Spacer()
Text("請(qǐng)耐心等待...")
}.padding(.trailing,10)
}else if context.state.driverStatus == "2"{
VStack{
Image("uav-smwait")
Spacer()
Text("請(qǐng)耐心等待...")
}.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) {
}
}
//下面是緊湊展示內(nèi)容區(qū)(只展示一個(gè)時(shí)的視圖)
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)
}
}
//當(dāng)多個(gè)Live Activities處于活動(dòng)時(shí)仗谆,展示此處極小視圖
minimal: {
VStack(alignment: .center) {
Image(systemName: "timer")
}
}
.keylineTint(.accentColor)
.widgetURL(URL(string: "cjh-my"))
}
}
}
靈動(dòng)島頁(yè)面需要實(shí)現(xiàn)的部分有4個(gè)
不支持靈動(dòng)島的機(jī)型 或 鎖屏?xí)r的 顯示
緊湊級(jí)展示(即左右貼合靈動(dòng)島的展示)
多Live activity時(shí)的展示(即極小視圖,左貼合淑履,右分離)
拓展視圖(長(zhǎng)按時(shí)觸發(fā))
5. 完成上面步驟隶垮,Live activity 已經(jīng)創(chuàng)建好了,下面是啟動(dòng):
(下面代碼包含和極光推送交互的代碼秘噪,通過(guò)激光推送更新數(shù)據(jù)狸吞,把liveactivityId發(fā)送極光推送)
import UIKit
Import ActivityKit
import SwiftUI
class UavManage: NSObject {
@available(iOS 16.1,*)
@objc func startDeliveryAction(liveactivityId:String) {
//初始化靜態(tài)數(shù)據(jù)
let uavAttributes = UavAttributes()
//初始化動(dòng)態(tài)數(shù)據(jù)
let initialContentState = UavAttributes.UavStatus(distance: "10", driverStatus: "3", progress: 50)
do {
//啟用靈動(dòng)島
//靈動(dòng)島只支持Iphone,areActivitiesEnabled用來(lái)判斷設(shè)備是否支持指煎,即便是不支持的設(shè)備蹋偏,依舊可以提供不支持的樣式展示
if ActivityAuthorizationInfo().areActivities Enabled == true{
}
let deliveryActivity = try Activity<UavAttributes>.request(
attributes: uavAttributes,
contentState: initialContentState,
pushType: PushType.token)
Task {
// 監(jiān)聽(tīng) push token 更新
to await pushToken in deliveryActivity.pushTokenUpdates {
let pushtokenstr = pushToken.map { String(format: "%02.2hhx", arguments: [$0]) }.joined()
// 向極光注冊(cè)liveactivity,在推送平臺(tái)通過(guò)liveactivityId推送通知進(jìn)行更新
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)")
}
}
}
靈動(dòng)島的活動(dòng)狀態(tài)一共有3種:
- active 處于活動(dòng)中
- ended 已經(jīng)終止且不會(huì)有任何更新贯要,但依舊在鎖屏界面展示
- dismissed 結(jié)束且不再展示
6. 更新靈動(dòng)島數(shù)據(jù)(更新的就是在ActivityAttributes中聲明的動(dòng)態(tài)數(shù)據(jù)):
下面的圖片我是用別人的暖侨,本項(xiàng)目通過(guò)消息推送更新數(shù)據(jù)的
func updateDeliveryPizza(){
Task {
let updatedDeliveryStatus =
PizzaDeliveryAttributes.PizzaDeliveryStatus (driverName:"快遞小哥" deliveryTimer: Date()... Date() .addingTimeInterval (60 * 60))
//此處只有一個(gè)靈動(dòng)島,當(dāng)一個(gè)項(xiàng)目有多個(gè)靈動(dòng)島時(shí)崇渗,需要判斷更新對(duì)應(yīng)的activity
for activity in Activity<PizzaDeliveryAttributes>.activitiest{
await activity.update(using: updatedDeliveryStatus)
}
}
}
Live activity也支持遠(yuǎn)程推送更新字逗,根據(jù)文檔以下9點(diǎn)要求實(shí)現(xiàn)(Activity遠(yuǎn)程通知每小時(shí)有通知預(yù)算<數(shù)量未明確>,超出后系統(tǒng)將關(guān)閉通知)
1. 確保主程序已經(jīng)開(kāi)通了遠(yuǎn)程推送功能
2. 確保啟動(dòng)activity時(shí)[request(attributes:contentState:pushType:)傳入pushType參數(shù)(.token)
3. 獲取啟動(dòng)后的activity的推送令牌pushToken宅广,傳給服務(wù)端用來(lái)推送更新activity
4. 服務(wù)端推送的更新內(nèi)容字段需要和ActivityAttributes的ContentState 中定義的動(dòng)態(tài)數(shù)據(jù)字段對(duì)應(yīng)
5. 設(shè)置推送的報(bào)頭apns-push-type的值為liveactivity
6. 設(shè)置推送的報(bào)頭apns-topic的值為<your bundleID>.push-type.liveactivity
7. 正確的推送對(duì)應(yīng)的內(nèi)容和狀態(tài)
8. 使用pushTokenUpdates監(jiān)聽(tīng)pushToken變化葫掉,如有變化,就令牌失效跟狱,需要將新的令牌傳給服務(wù)器
9. 當(dāng)Activity結(jié)束時(shí)俭厚,服務(wù)器端的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內(nèi)設(shè)置dissall -date 字段來(lái)告訴系統(tǒng)在什么時(shí)候移除activity ,eg: “dismissal-date”: 1663177260
不用為推送提供聲音 , 如果推送延遲驶臊,在activity結(jié)束后收到時(shí)將被忽略挪挤,avtivity每小時(shí)有通知預(yù)算(數(shù)量未明確)叼丑,超出后系統(tǒng)將關(guān)閉通知
7. 結(jié)束Activity:
func stopDeliveryPizza () {
Task {
for activity in Activity<PizzaDeliveryAttributes>.activitiest{
await activitv. end(dismissalPolicv: . immediate)
}
}
}
結(jié)束分為兩種:
.default 系統(tǒng)默認(rèn),結(jié)束后在鎖屏界面保留4小時(shí)
.immediate 立即結(jié)束扛门,不會(huì)在鎖屏界面停留
下面是通過(guò)極光推送實(shí)現(xiàn)的方式鸠信,實(shí)時(shí)活動(dòng)數(shù)據(jù)的更新和結(jié)束
- 消息內(nèi)容:添加的是動(dòng)態(tài)數(shù)據(jù),結(jié)束和更新相比會(huì)多一個(gè)實(shí)時(shí)活動(dòng)結(jié)束的時(shí)間论寨;
- 目標(biāo)群體:是之前通過(guò)代碼給極光推送注冊(cè)的liveactivityId星立;
- 下面的圖是手動(dòng)推送的方式,后面主要通過(guò)后臺(tái)和極光推送交互進(jìn)行推送葬凳;
- 有個(gè)注意點(diǎn)是模擬器不會(huì)產(chǎn)生臨時(shí)令牌绰垂,最終還是通過(guò)真機(jī)進(jìn)行測(cè)試。
參考文檔:http://www.reibang.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