零、关于灵动岛的认识
灵动岛,即实时活动(Live Activity)
它答应人们以瞥见
的形式来观察事情或使命的状态.我的了解是”我不需求一向盯着看,可是我偶尔想看的时分能很方便的看到”.这就需求再规划的时分尽或许扔掉没用的信息,保持信息的简练.
实时活动的事情构成最好是包括清晰开端 + 完毕
的事情.例如:外卖、球赛等.
实时活动在完毕前最多存活8小时,完毕后在锁屏界面最多再保存4小时.
关于更多灵动岛(实时活动)的最佳实践及规划思路能够参阅一下: 知乎-苹果开放第三方App登岛,灵动岛规划指南来了!
一、灵动岛的UI布局
接入灵动岛后,有的设备支持(iPhone14Pro / iPhone14ProMax)展现灵动岛+告诉中心,有的设备不支持灵动岛则只在告诉中心展现一条实时活动的告诉. 所以以下四种UI都需求实现:
1.紧凑型 2.最小型 3.扩展型 4.告诉
二、代码实现
1.在主工程中创立灵动岛Widget工程
Xcode -> Editor -> Add Target
如图勾选即可
2.在主工程的info.plist中增加key
Supports Live Activities
= YES (答应实时活动)
Supports Live Activities Frequent Updates
= YES(实时活动支持频频更新) 这个看项目的需求,不是强制的
3.增加主工程与widget数据交互模型
在主工程中,新建Swift File,作为交互模型的文件.这儿将数据管理与模型都放到这一个文件里了.
创立文件后的目录结构
import Foundation
import ActivityKit
//整个数据交互的模型
struct TestWidgetAttributes: ActivityAttributes {
public typealias TestWidgetState = ContentState
//可变参数(动态参数)
public struct ContentState: Codable, Hashable {
var data: String
}
//不可变参数 (整个实时活动都不会改动的参数)
var id: String
}
如果参数过多.或者与OC混编,默许给出的这种结构体或许无法满意要求.此时能够运用独自的模型目标,这样OC中也可直接结构与赋值.注意,此处的模型需求遵从Codable
协议
import Foundation
import ActivityKit
struct TestWidgetAttributes: ActivityAttributes {
public typealias TestWidgetState = ContentState
//可变参数(动态参数)
public struct ContentState: Codable, Hashable {
var dataModel: TestLADataModel
}
//不可变参数 (整个实时活动都不会改动的参数)
//var name: String
}
@objc public class TestLADataModel: NSObject, Codable {
@objc var idString : String = ""
@objc var nameDes : String = ""
@objc var contentDes : String = ""
@objc var completedNum : Int//已完结人数
@objc var notCompletedNum : Int//未完结人数
var allPeopleNum : Int {
get {
return completedNum + notCompletedNum
}
}
public override init() {
self.nameDes = ""
self.contentDes = ""
self.completedNum = 0
self.notCompletedNum = 0
super.init()
}
/// 便当结构
@objc convenience init(nameDes: String, contentDes: String, completedNum: Int, notCompletedNum: Int) {
self.init()
self.nameDes = nameDes
self.contentDes = contentDes
self.completedNum = completedNum
self.notCompletedNum = notCompletedNum
}
}
4.Liveactivity widget的UI
打开前文创立的widget,我的叫demoWLiveActivity.swift
这儿给出了默许代码的注释,详细的布局代码就不再此处赘述了.
import ActivityKit
import WidgetKit
import SwiftUI
struct demoWLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: demoWAttributes.self) { context in
// 锁屏之后,显现的桌面告诉栏方位,这儿能够做相对杂乱的布局
VStack {
Text("Hello")
}
.activityBackgroundTint(Color.cyan)
.activitySystemActionForegroundColor(Color.black)
} dynamicIsland: { context in
DynamicIsland {
/*
这儿是长按灵动岛[扩展型]的UI
有四个区域限制了布局,分别是左、右、中间(硬件下方)、底部区域
*/
DynamicIslandExpandedRegion(.leading) {
Text("Leading")
}
DynamicIslandExpandedRegion(.trailing) {
Text("Trailing")
}
DynamicIslandExpandedRegion(.center) {
Text("Center")
}
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)
}
}
}
5.Liveactivity 的发动 / 更新(主工程) / 中止
发动
let attributes = TestWidgetAttributes()
let initialConetntState = TestWidgetAttributes.TestWidgetState(dataModel: dataModel)
do {
let activity = try Activity.request(
attributes: attributes,
content: .init(state: initialConetntState, staleDate: nil),
pushType: nil
// pushType: .token
)
print("恳求敞开实时活动: \(activity.id)")
} catch (let error) {
print("恳求敞开实时犯错: \(error.localizedDescription)")
}
更新
let updateState = TestWidgetAttributes.TestWidgetState(dataModel: dataModel)
let alertConfig = AlertConfiguration(
title: "\(dataModel.nameDes) has taken a critical hit!",
body: "Open the app and use a potion to heal \(dataModel.nameDes)",
sound: .default
)
await activity.update(
ActivityContent<TestWidgetAttributes.ContentState>(
state: updateState,
staleDate: nil
),
alertConfiguration: alertConfig
)
print("更新实时活动: \(activity.id)")
完毕
let finalContent = TestWidgetAttributes.ContentState(
dataModel: TestLADataModel()
)
let dismissalPolicy: ActivityUIDismissalPolicy = .default
await activity.end(
ActivityContent(state: finalContent, staleDate: nil),
dismissalPolicy: dismissalPolicy)
removeActivityState(id: idString);
print("完毕实时活动: \(activity)")
三、更新数据
数据的更新主要经过两种办法:
1.服务端推送
2.主工程更新
其中主工程的更新参见(2.5.Liveactivity 的发动 / 更新(主工程) / 中止)
这儿主要讲经过推送办法的更新
首要为主工程敞开推送功能,但不要运用registerNotifications()
为ActivityKit推送告诉注册您的实时活动,详细的注册办法见下.
1. APNs 认证办法挑选
APNs认证办法分为两种: 1.cer证书认证
2.Token-Based认证办法
此处只能挑选Token-Based认证办法,挑选cer证书认证发送LiveActivity推送时,会报TopicDisallowed过错.
Token-Based认证办法的key生产办法 参见:Apple Documentation – Establishing a token-based connection to APNs
2. Liveactivity 的发动
let attributes = TestWidgetAttributes()
let initialConetntState = TestWidgetAttributes.TestWidgetState(dataModel: dataModel)
do {
let activity = try Activity.request(
attributes: attributes,
content: .init(state: initialConetntState, staleDate: nil),
pushType: .token//须运用此值,声明发动需求获取token
)
//判别发动成功后,获取推送令牌 ,发送给服务器,用于远程推送Live Activities更新
//不是每次发动都会成功,当现已存在多个Live activity时会出现发动失利的状况
print("恳求敞开实时活动: \(activity.id)")
Task {
for await pushToken in activity.pushTokenUpdates {
let pushTokenString = pushToken.reduce("") { $0 + String(format: "%02x", $1) }
//这儿拿到用于推送的token,将该值传给后端
pushTokenDidUpdate(pushTokenString, pushToken);
}
}
} catch (let error) {
print("恳求敞开实时犯错: \(error.localizedDescription)")
}
3. 模仿推送
1.能够运用terminal简略的构建推送,这儿随便放上一个栗子
2.也能够运用这个东西 – SmartPushP8
此处运用SmartPushP8
宣布推送后,在设备上查看推送成果即可.
注意:模仿器也是能够收到liveactivity的推送的可是需求满意:运用T2安全芯片 or 运行macOS13以上的 M1 芯片设备
四、问题排查
推送失利:
1.TooManyProviderTokenUpdates
测验环境对推送次数有一定的限制.测验切换线路(sandbox / development)能够获得更多推送次数.
如果切换线路无法解决问题,建议重新run一遍工程,这样能够获得新的deviceToken,完结推送测验.
2.InvalidProviderToken / InternalServerError
测验重新挑选证书,重新run工程吧…暂时无解.
推送成功,但设备并未收到更新
这种状况需求打开控制台
来观察日志
1.挑选对应设备
2.点击过错和故障
3.过滤条件中增加这三项进程
liveactivitiesd
apsd
chronod
4.点击开端
鄙人方能够看到过错日志.
demo参阅:demo
五、参阅链接
AppleDeveloper – ActivityKit
AppleDeveloper – Sending notification requests to APNs
HarryDeng – iOS灵动岛开发实践
Xu Jiwei – iOS 运用推送告诉更新 Dynamic Island 和 Live Activity