iOS-Live Activity開(kāi)發(fā)

文檔

官方文檔
官方文檔-用戶交互指南
官方文檔-構(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)擊 + 添加
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市审编,隨后出現(xiàn)的幾起案子撼班,更是在濱河造成了極大的恐慌,老刑警劉巖垒酬,帶你破解...
    沈念sama閱讀 219,188評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件砰嘁,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡勘究,警方通過(guò)查閱死者的電腦和手機(jī)矮湘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)口糕,“玉大人缅阳,你說(shuō)我怎么就攤上這事【懊瑁” “怎么了十办?”我有些...
    開(kāi)封第一講書人閱讀 165,562評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)伏伯。 經(jīng)常有香客問(wèn)我橘洞,道長(zhǎng),這世上最難降的妖魔是什么说搅? 我笑而不...
    開(kāi)封第一講書人閱讀 58,893評(píng)論 1 295
  • 正文 為了忘掉前任炸枣,我火速辦了婚禮,結(jié)果婚禮上弄唧,老公的妹妹穿的比我還像新娘适肠。我一直安慰自己,他們只是感情好候引,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布侯养。 她就那樣靜靜地躺著,像睡著了一般澄干。 火紅的嫁衣襯著肌膚如雪逛揩。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 51,708評(píng)論 1 305
  • 那天麸俘,我揣著相機(jī)與錄音辩稽,去河邊找鬼。 笑死从媚,一個(gè)胖子當(dāng)著我的面吹牛逞泄,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,430評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼喷众,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼各谚!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起到千,我...
    開(kāi)封第一講書人閱讀 39,342評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤昌渤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后憔四,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體愈涩,經(jīng)...
    沈念sama閱讀 45,801評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評(píng)論 3 337
  • 正文 我和宋清朗相戀三年加矛,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片煤篙。...
    茶點(diǎn)故事閱讀 40,115評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡斟览,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出辑奈,到底是詐尸還是另有隱情苛茂,我是刑警寧澤,帶...
    沈念sama閱讀 35,804評(píng)論 5 346
  • 正文 年R本政府宣布鸠窗,位于F島的核電站妓羊,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏稍计。R本人自食惡果不足惜躁绸,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望臣嚣。 院中可真熱鬧净刮,春花似錦、人聲如沸硅则。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,008評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)怎虫。三九已至暑认,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間大审,已是汗流浹背蘸际。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,135評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留饥努,地道東北人捡鱼。 一個(gè)月前我還...
    沈念sama閱讀 48,365評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親驾诈。 傳聞我的和親對(duì)象是個(gè)殘疾皇子缠诅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容