注册

iOS 灵动岛上岛指南

零、关于灵动岛的认识

灵动岛,即实时活动(Live Activity)

它允许人们以瞥见的形式来观察事件或任务的状态.我的理解是"我不需要一直盯着看,但是我偶尔想看的时候能很方便的看到".这就需要再设计的时候尽可能扔掉没用的信息,保持信息的简洁.

实时活动的事件构成最好是包含明确开始 + 结束的事件.例如:外卖、球赛等.

实时活动在结束前最多存活8小时,结束后在锁屏界面最多再保留4小时.

关于更多灵动岛(实时活动)的最佳实践及设计思路可以参考一下:
知乎-苹果开放第三方App登岛,灵动岛设计指南来了!

一、灵动岛的UI布局

接入灵动岛后,有的设备支持(iPhone14Pro / iPhone14ProMax)展示灵动岛+通知中心,有的设备不支持灵动岛则只在通知中心展示一条实时活动的通知.
所以以下四种UI都需要实现:

1.紧凑型

887c0a40b9301020eb545acc531fc5df.jpg

2. 最小型

00b0e9e441f62f14f357064d079b83d4.jpg

3. 扩展型

74371cee6f752aa2675dadf4a7e620bf.jpg

4. 通知

4dda19544ceabfccd341dc97e8b0bd1f.jpg

二、代码实现

1.在主工程中创建灵动岛Widget工程

Xcode -> Editor -> Add Target

297f5a85c73484b3be07f53bfcfb6c84.jpg

如图勾选即可

2034c9a593f05635eddc17d1feafd995.jpg

2.在主工程的info.plist中添加key

Supports Live Activities = YES (允许实时活动)

Supports Live Activities Frequent Updates = YES(实时活动支持频繁更新) 这个看项目的需求,不是强制的

56a25df0e35c53f5d89d8314f0d2d3b9.jpg

3.添加主工程与widget数据交互模型

在主工程中,新建Swift File,作为交互模型的文件.这里将数据管理与模型都放到这一个文件里了.

71834cb44b25f12eb200d8ebc1f12f48.jpg

创建文件后的目录结构

1a230fc97c4004b7a33f18bef0abb884.jpg

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推送通知注册您的实时活动,具体的注册方法见下.
19afbd4d76b9be4d65e8c579688b0a4f.jpg

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: "x", $1) }
//这里拿到用于推送的token,将该值传给后端
pushTokenDidUpdate(pushTokenString, pushToken);
}
}
} catch (let error) {
print("请求开启实时出错: \(error.localizedDescription)")
}

3. 模拟推送

1.可以使用terminal简单的构建推送,这里随便放上一个栗子

2.也可以使用这个工具 - SmartPushP8

此处使用SmartPushP8

45caccc03678b25e36e49e1e5d3bc965.jpg

发出推送后,在设备上查看推送结果即可.

注意:模拟器也是可以收到liveactivity的推送的但是需要满足:使用T2安全芯片 or 运行macOS13以上的 M1 芯片设备

四、问题排查

推送失败:

1.TooManyProviderTokenUpdates

测试环境对推送次数有一定的限制.尝试切换线路(sandbox / development)可以获得更多推送次数.

如果切换线路无法解决问题,建议重新run一遍工程,这样可以获得新的deviceToken,完成推送测试.

2.InvalidProviderToken / InternalServerError

尝试重新选择证书,重新run工程吧...暂时无解.

推送成功,但设备并未收到更新

这种情况需要打开控制台来观察日志

1.选择对应设备

2.点击错误和故障

3.过滤条件中添加这三项进程

liveactivitiesd
apsd
chronod

4.点击开始

345db1b45a5fa62d70a2a1f40ebbcc30.jpg在下方可以看到错误日志.

demo参考:demo


作者:大功率拖拉机
链接:https://juejin.cn/post/7254101170951192613
来源:稀土掘金

0 个评论

要回复文章请先登录注册