文檔
官方文檔
官方文檔-用戶交互指南
官方文檔-構(gòu)建推送消息
主要參考文檔
參考文檔
開(kāi)發(fā)須知
- 強(qiáng)烈建議閱讀下官方文檔
- Live Activity 包括兩部分诫惭,靈動(dòng)島 和 鎖定屏幕(通知中心)汪诉,且必須對(duì)所有樣式進(jìn)行支持。
- 僅 iOS 16.1 以上支持摊腋,支持iPhone,iPad近忙,詳見(jiàn)官方用戶交互指南文檔
- ActivityKit用于管理Live Activities的生命周期骄酗。(request、update邦邦、end)
- 支持隱式動(dòng)畫
- 如果用戶或者App沒(méi)有主動(dòng)終止Live Activity安吁,靈動(dòng)島上最多可以存在8個(gè)小時(shí),然后就會(huì)被系統(tǒng)終止燃辖。 如果在鎖定屏幕上鬼店,它可以存活12小時(shí)
- Live Activity 使用的圖片資源,要小于或等于Live Activity的大小黔龟。如果大于的話妇智,可能無(wú)法啟動(dòng)。
- 每個(gè)Live Activity 有自己的沙盒
- 無(wú)法訪問(wèn)網(wǎng)絡(luò)氏身,無(wú)法使用定位功能巍棱。
- 可以通過(guò) ActivityKit 或者 ActivityKit push notifications 來(lái)配置、啟動(dòng)蛋欣、更新與終止 Live Activity但二者在更新時(shí)的動(dòng)態(tài)數(shù)據(jù)大小均不能超過(guò) 4 KB
- 每個(gè) Activity 對(duì)應(yīng)一個(gè)推送token航徙,Activity 中止后,令牌失效豁状。
- 使用 ActivityKit push notifications 更新捉偏,每小時(shí)會(huì)有一定的預(yù)算,官方文檔沒(méi)具體說(shuō)是多少泻红。超過(guò)預(yù)算的消息有可能被系統(tǒng)限制夭禽。解決方案有兩個(gè),方案一是服務(wù)端在推消息的時(shí)候谊路,手動(dòng)指定消息優(yōu)先級(jí)讹躯,消息優(yōu)先級(jí)10(也是默認(rèn)優(yōu)先級(jí))會(huì)被加入預(yù)算,消息優(yōu)先級(jí)5不會(huì)被加入預(yù)算缠劝。方案二是在主工程的 info.plist 中加入 NSSupportsLiveActivitiesFrequentUpdates 并設(shè)為 YES潮梯,添加后會(huì)在 App 的設(shè)置里面,實(shí)時(shí)活動(dòng)選項(xiàng)不再是開(kāi)關(guān)惨恭,而是可以點(diǎn)進(jìn)二級(jí)頁(yè)秉馏,二級(jí)頁(yè)里面會(huì)有 更頻繁更新 的開(kāi)關(guān),用戶可以隨時(shí)關(guān)掉脱羡。這個(gè)選項(xiàng)也可以通過(guò)代碼獲取狀態(tài)及監(jiān)聽(tīng)狀態(tài)變更萝究。
- 可以通過(guò)Link實(shí)現(xiàn)點(diǎn)擊不同區(qū)域免都,跳轉(zhuǎn)到不同頁(yè)面。如果只使用widgetURL的方式跳轉(zhuǎn)帆竹,那么僅可以跳轉(zhuǎn)到一個(gè)scheme頁(yè)面绕娘。
- 鎖屏小組件和靈動(dòng)島,共用一份數(shù)據(jù)栽连,更新時(shí)同步更新险领。可以設(shè)計(jì)為不同的UI樣式秒紧。
開(kāi)發(fā)流程
- 創(chuàng)建 Widget Extension绢陌,并確保 Include Live Activity是勾選上的
- 在主工程中添加 NSSupportsLiveActivities 并設(shè)為 YES
- 如需要更頻繁的推送,在主工程中添加 NSSupportsLiveActivitiesFrequentUpdates 并設(shè)為 YES
- 在主工程中噩茄,點(diǎn)擊Project - Target - Signing & Capabilities下面,確認(rèn)添加了 Push Notificatioins 功能
- 創(chuàng)建好 Widget,會(huì)自動(dòng)生成模版代碼绩聘,只需要在上面修改相應(yīng)代碼即可
// 入口代碼沥割,確定需要加載 Live Activity
@main
struct PizzaDeliveryWidgets: WidgetBundle {
var body: some Widget {
// widget
FavoritePizzaWidget()
// Live Activity
if #available(iOS 16.1, *) {
PizzaDeliveryLiveActivity()
}
}
}
// 配置數(shù)據(jù)
struct PizzaDeliveryAttributes: ActivityAttributes {
public typealias PizzaDeliveryStatus = ContentState
// 動(dòng)態(tài)數(shù)據(jù)
public struct ContentState: Codable, Hashable {
var driverName: String
var deliveryTimer: ClosedRange<Date>
}
// 靜態(tài)數(shù)據(jù)
var numberOfPizzas: Int
var totalAmount: String
var orderNumber: String
}
// 配置UI,共計(jì)需要設(shè)計(jì)出 4 個(gè) UI凿菩,且全都需要實(shí)現(xiàn)
struct PizzaDeliveryActivityWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: PizzaDeliveryAttributes.self) { context in
// 鎖屏UI机杜,出現(xiàn)在所有設(shè)備上 - 第一個(gè)UI
// 不支持靈動(dòng)島的未鎖屏的設(shè)備上,顯示為 banner UI
// 兩個(gè)都使用同一個(gè)View組件衅谷,在這里配置
// 系統(tǒng)使用默認(rèn)的文本顏色和最適合鎖定屏幕的實(shí)時(shí)活動(dòng)背景色
VStack {
Text("Your \(context.state.driverName) is on the way!")
}
.activityBackgroundTint(Color.cyan) // 修改背景顏色
.activitySystemActionForegroundColor(Color.black) // 修改文本顏色
} dynamicIsland: { context in
// 靈動(dòng)島UI
DynamicIsland {
// 展開(kāi)后的UI - 第二個(gè)UI
// 需要組合不同的區(qū)域
DynamicIslandExpandedRegion(.leading) {
// 展開(kāi)后的前面
Text("Leading")
}
DynamicIslandExpandedRegion(.trailing) {
// 展開(kāi)后的后面
Text("Trailing")
}
DynamicIslandExpandedRegion(.center) {
// 展開(kāi)后的中間
Text("Center")
}
DynamicIslandExpandedRegion(.bottom) {
// 展開(kāi)后的底部
Text("Bottom")
}
} compactLeading: {
// 緊湊樣式的左邊 - 第三個(gè)UI(左)
Text("L")
} compactTrailing: {
// 緊湊樣式的右邊 - 第三個(gè)UI(右)
Text("T")
} minimal: {
// 當(dāng)有多個(gè) Live Activity時(shí)椒拗,靈動(dòng)島顯示成circular minimal 樣式 - 第四個(gè)UI
Text("Min")
}
.widgetURL(URL(string: "demo://homepage")) // 點(diǎn)擊跳轉(zhuǎn)到指定頁(yè)面
.keylineTint(Color.red)
}
}
}
- 修改主項(xiàng)目工程文件,配置获黔、啟動(dòng)蚀苛、更新與終止 Live Activity
// 啟動(dòng) Live Activity
func startDeliveryPizza() {
// 判斷版本號(hào)
guard #available(iOS 16.1, *) else {
return
}
// 判斷是否開(kāi)啟了 Live Activity 權(quán)限
guard ActivityAuthorizationInfo().areActivitiesEnabled else {
// Live Activity 不可用,上報(bào)空 token 給服務(wù)端
uploadTokenToService(nil)
return
}
// 創(chuàng)建數(shù)據(jù)
let pizzaDeliveryAttributes = PizzaDeliveryAttributes(numberOfPizzas: 1, totalAmount:"$99")
let initialContentState = PizzaDeliveryAttributes.PizzaDeliveryStatus(driverName: "TIM ??????", estimatedDeliveryTime: Date()...Date().addingTimeInterval(15 * 60))
do {
// 請(qǐng)求啟動(dòng) Live Activity
let deliveryActivity = try Activity<PizzaDeliveryAttributes>.request(
attributes: pizzaDeliveryAttributes,
contentState: initialContentState,
pushType: .token) // Enable Push Notification Capability First (from pushType: nil)
print("Requested a pizza delivery Live Activity \(deliveryActivity.id)")
Task {
// 監(jiān)聽(tīng) push token 更新
for await pushToken in deliveryActivity.pushTokenUpdates {
let pushTokenString = pushToken.reduce("") { $0 + String(format: "%02x", $1) }
print(pushTokenString)
// 上傳 push token 給服務(wù)端玷氏,用于推送更新 Live Activity
uploadTokenToService(pushTokenString)
}
}
Task {
// 監(jiān)聽(tīng) state 數(shù)據(jù)內(nèi)容變化
for await state in deliveryActivity.contentStateUpdates {
print("1content state update: tip=\(state.driverName)")
}
}
Task {
// 監(jiān)聽(tīng) Activity 狀態(tài)變化
for await state in deliveryActivity.activityStateUpdates {
print("activity state update: tip=\(state) id:\(deliveryActivity.id)")
// 當(dāng) LiveActivity 結(jié)束時(shí)堵未,使服務(wù)端的推送token失效
// LiveActivity 活動(dòng)狀態(tài)一共有 4 種
// .active 處于活動(dòng)中
// .ended 已經(jīng)終止且不會(huì)有任何更新,但依舊在鎖屏界面展示
// .dismissed 結(jié)束且不再展示
// .stale 消息過(guò)時(shí)盏触,等待最新的消息渗蟹。(iOS 16.2 以上才支持)
if state == .ended || state == .dismissed {
uploadTokenToService(nil)
}
}
}
} catch (let error) {
print("Error requesting pizza delivery Live Activity \(error.localizedDescription)")
// Live Activity 不可用,上報(bào)空 token 給服務(wù)端
uploadTokenToService(nil)
}
}
// 更新 Live Activity
func updateDeliveryPizza() {
// 判斷版本號(hào)
guard #available(iOS 16.1, *) else {
return
}
Task {
// 獲取數(shù)據(jù)
let updatedDeliveryStatus = PizzaDeliveryAttributes.PizzaDeliveryStatus(driverName: "TIM ??????", estimatedDeliveryTime: Date()...Date().addingTimeInterval(60 * 60))
// 更新數(shù)據(jù)
for activity in Activity<PizzaDeliveryAttributes>.activities{
// 用戶可以在鎖定屏幕上移除Live Activity 后赞辩,ActivityState會(huì)變?yōu)?dismissed雌芽。
if activity.activityState == .dismissed {
continue
}
await activity.update(using: updatedDeliveryStatus)
}
}
}
// 結(jié)束 Live Activity
func stopDeliveryPizza() {
// 判斷版本號(hào)
guard #available(iOS 16.1, *) else {
return
}
Task {
// dismissalPolicy 有三種
// .default 會(huì)在鎖屏屏幕上停留四個(gè)小時(shí),以便用戶查看最后一個(gè)消息辨嗽,或用戶主動(dòng)移除
// .immediate 立即結(jié)束世落,不會(huì)在屏幕上停留
// .after() 指定時(shí)間結(jié)束,最長(zhǎng)為當(dāng)前時(shí)間+4小時(shí)
for activity in Activity<PizzaDeliveryAttributes>.activities{
// 用戶可以在鎖定屏幕上移除Live Activity 后糟需,ActivityState會(huì)變?yōu)?dismissed岛心。
if activity.activityState == .dismissed {
continue
}
await activity.end(dismissalPolicy: .immediate)
}
print("Cancelled pizza delivery Live Activity")
}
}
// 展示所有 Live Activity
func showAllDeliveries() {
// 判斷版本號(hào)
guard #available(iOS 16.1, *) else {
return
}
Task {
for activity in Activity<PizzaDeliveryAttributes>.activities {
print("Pizza delivery details: \(activity.id) -> \(activity.attributes)")
}
}
}
推送更新數(shù)據(jù)
- 主工程開(kāi)通遠(yuǎn)程推送功能
- 主工程啟動(dòng) LiveActivity来破,獲取 pushToken,且監(jiān)聽(tīng) pushToken 變化忘古,獲取變化后的pushToken
- 將 pushToken 通過(guò)API上報(bào)給服務(wù)端
- 服務(wù)端設(shè)置推送消息的Header header field,將 apns-push-type 字段的值設(shè)置為liveactivity诅诱,將 apns-topic 字段的值設(shè)置為 <your bundleID>.push-type.liveactivity
- 服務(wù)端設(shè)置推送消息的 json髓堪,"aps" 字段下的 "content-state" 字段,要和客戶端 ActivityAttributes 的 ContentState 中定義的動(dòng)態(tài)數(shù)據(jù)字段名相對(duì)應(yīng)
- 服務(wù)端設(shè)置推送消息的 json娘荡,"aps" 字段下的 "events"干旁,設(shè)置為 "update" 或 "end"。如果是end炮沐,需要確保 content-state 數(shù)據(jù)為最終的數(shù)據(jù)狀態(tài)争群,因?yàn)橹驦iveActivity就不可以再更新了
- 如果主App存活期間, LiveActivity 被標(biāo)記為結(jié)束時(shí)大年,需要調(diào)用接口换薄,將服務(wù)端的 pushToken 置為失效。如果是服務(wù)端推送 end 狀態(tài)翔试,將 LiveActivity 置為結(jié)束轻要,需要服務(wù)端主動(dòng)將 pushToken 置為失效
- 擴(kuò)展 - 可在 "aps" 下設(shè)置 "stale-date" 字段,確保 LiveActivity 不會(huì)展示過(guò)時(shí)的消息內(nèi)容垦缅。例如冲泥,用戶斷網(wǎng),沒(méi)有收到最新的 update 推送消息壁涎,如果到達(dá)了 "stale-date" 的時(shí)間凡恍,LiveActivity 的狀態(tài)會(huì)自動(dòng)變?yōu)?stale,顯示消息已過(guò)時(shí)的 UI怔球,等最新的消息到達(dá)后嚼酝,或用戶主動(dòng)操作后,再更新 UI 為其他的狀態(tài)庞溜。但 stale 狀態(tài)支持需 iOS 16.2 以上
- 擴(kuò)展 - 可在 "aps" 下設(shè)置 "dismissal-date" 字段革半,修改默認(rèn)的 end 狀態(tài)停留時(shí)長(zhǎng)。如果不設(shè)置此字段流码,且結(jié)束時(shí)設(shè)置的 dismissalPolicy 為 .default又官,那么結(jié)束狀態(tài)的 UI 會(huì)在鎖屏界面默認(rèn)停留4個(gè)小時(shí),除非用戶主動(dòng)移除它漫试。
// 官方提供的推送消息內(nèi)容示例
{
"aps": {
"timestamp": 1168364460,
"events": "update",
"relevance-score": 75.0,
"stale-date": 1650998941,
"content-state": {
"driverName": "Anne Johnson",
"estimatedDeliveryTime": 1659416400
},
"alert": {
"title": "Delivery Update",
"body": "Your pizza order will arrive soon.",
"sound": "example.aiff"
}
}
}
常見(jiàn)問(wèn)題
The operation couldn’t be completed. (com.apple.ActivityKit.ActivityInput error 1.)
// 檢查主工程的info里面六敬,是否添加了NSSupportsLiveActivities并設(shè)置為YES
The operation couldn’t be completed. (com.apple.ActivityKit.ActivityInput error 0.)
// 檢查主工程,是否添加了推送功能驾荣。Project - Target - Signing & Capabilities外构,如果沒(méi)有 Push Notificatioins普泡,則點(diǎn)擊 + 添加