简介

之前在 《iOS16新特性:灵动岛适配开发与到家业务场景结合的探索实践》 里介绍了iOS16新的特性:实时更新(Live Activity)中灵动岛的适配流程,但其实除了灵动岛的展示样式,Live Activity还有一种非常实用的应用场景,那就是锁屏界面实时状态更新:

上图是部分已经做出适配的APP,锁屏实时活动的展示。可以看到,相比于灵动岛的样式,锁屏更新的展示区域更大,能够显示更多信息,并且是在锁屏界面上进行展示,结合苹果在iPhone14之后推出的“全天候显示”功能,能够让用户在不解锁手机,甚至不拿起手机的情况下就能够获取到APP内最新的消息更新,在某些应用场景下非常实用。

这篇文章主要就介绍Live Activity中锁屏实时活动样式的适配流程,再结合实际开发过程中的遇到的问题进行实际详解:

限制条件

在进行开发之前,需要先了解一下锁屏实时活动的一些限制条件:

1.实时活动显示在通知区域且有更自由的视图定制和刷新方法,但是跟Widget小组件一样,它也限制了视图上的动画开发,所有的动画效果仅能由系统处理。

2.锁屏通知区域内的实时活动在8小时之内可以刷新数据展示,超过8小时不再支持刷新,,超过12小时强制消失

3.实时活动视图本体不支持发起网络请求,所有的动态数据都要经由通知下发,或者后台活动数据刷新,且每次更新的数据不能超过4KB。

4.实时活动可以通过推送下发更新数据,但是推送的类型不同于传统“基于证书”的推送,而是“基于token”的推送类型。

实际开发

1.建立锁屏实时活动扩展项目

这部分建立的过程与灵动岛的适配流程完全一致,请参见 iOS16新特性:灵动岛适配开发与到家业务场景结合的探索实践 中相关的流程描述,如果之前建立过灵动岛项目,则可以直接开始开发:

2.UI开发

Live Activity的全部样式开发均完全采用SwiftUI,锁屏实时活动也不例外,以下是我开发的UI部分代码,大家可以一参考一下:

struct LockScreenLiveActivityView: View {
let context: ActivityViewContext<DJDynamicIslandAttributes> var body: some View {
VStack {
Spacer(minLength: 10)
LockScreenLiveActivityStoreHeaderView(imageURL: context.state.logo, title: context.state.title, subTitle: context.state.subTitle)
Spacer(minLength: 0)
LockScreenLiveActivityProgressView(progress: context.state.progress)
Spacer(minLength: 10)
}
}
} struct LockScreenLiveActivityStoreHeaderView: View {
let imageURL: String
let title: String
let subTitle: String var body: some View {
HStack(spacing: 10) {
NetworkImage(imageUrl: imageURL)
.frame(width: 50, height: 50) VStack(alignment: .leading, spacing: 4) {
HStack {
Text(title)
.font(.system(size: 16, weight: .bold))
.foregroundColor(Color(hex: 0x333333, alpha: 1))
} Text(subTitle)
.font(.system(size: 13))
.foregroundColor(Color(hex: 0x666666, alpha: 1))
.padding(EdgeInsets(top: 5, leading: 0, bottom: 0, trailing: 0))
} Spacer() // 填充剩余空间
}
.padding(8)
}
} struct LockScreenLiveActivityProgressView: View {
var progress: CGFloat
let borderOffset = 20.0 var body: some View {
VStack {
ZStack(alignment: .bottom) {
HStack(alignment: .bottom) {
Spacer()
NetworkImage(imageUrl: "", placeholdImage: "store")
.frame(width: 50, height: 50)
Spacer()
} HStack(alignment: .bottom) {
NetworkImage(imageUrl: "", placeholdImage: "knight")
.frame(width: 40, height: 40)
.offset(x: progress * UIScreen.main.bounds.width - 25)
Spacer()
} HStack(alignment: .bottom) {
Spacer()
NetworkImage(imageUrl: "", placeholdImage: "pin")
.frame(width: 18, height: 25)
.offset(x: -borderOffset)
}
}
.frame(height: 50)
Spacer(minLength: 0)
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 5)
.foregroundColor(Color.gray)
.frame(height: 10) RoundedRectangle(cornerRadius: 5)
.foregroundColor(Color.yellow)
.frame(width: (UIScreen.main.bounds.width - borderOffset * 3) * progress, height: 10)
}
.frame(height: 15)
.padding(.horizontal, borderOffset)
}
}
}

运行起来以后大概长这个样子:

坑1:

由于实时活动不允许加载网络请求,所以网络图片的URL也无法加载,可以通过:

1.直接通推送通知过下发图片的Data,再转成img,但是要注意数据大小,不要超过4Kb

2.本地图片

来解决

3.Live Activity的生命周期

Live Activity的生命周期由ActivityKit管理,其中,数据部分的模型类为ActivityAttributes,自定义数据模型需要继承自ActivityAttributes,静态数据变量直接生命在结构体内,动态数据变量需要声明在ActivityAttributes的ContentState中,这部分变量在接收到推送更新数据时,会自动根据json数据的key值进行解析并更新:

struct DJDynamicIslandAttributes: ActivityAttributes {

    public typealias DJDynamicIslandStatus = ContentState

    public struct ContentState: Codable, Hashable {
// 动态数据
var logo: String = ""
var title: String = ""
var subTitle: String = ""
var progress: Double = 0
} // 静态数据
var totalAmount: String
var orderId: String
}

Live Activity的生命周期分为:

创建(start)

利用Activity的request方法创建

func startActivity() throws {

        let attributes = DJDynamicIslandAttributes(
// 静态数据
)
let initialContentState = DJDynamicIslandAttributes.ContentState(
// 动态数据
)
let activity = try Activity.request(
attributes: attributes,
content: .init(state: initialContentState, staleDate: nil),
pushType: .token)
}

更新(update)

利用Activity的update方法更新,传入的参数即为ActivityAttributes的ContentState,也就是动态数据部分

func updateActivity(){
Task{
let updatedStatus = DJDynamicIslandAttributes.ContentState(
// 动态数据
)
for activity in Activity<DJDynamicIslandAttributes>.activities{
await activity.update(using: updatedStatus)
print("已更新灵动岛显示 Value值已更新 请展开灵动岛查看")
}
}
}

结束(end)

利用Activity的end方法结束,并从锁屏通知界面上移除

func endActivity(){
Task{
for activity in Activity<DJDynamicIslandAttributes>.activities{
await activity.end(dismissalPolicy: .immediate)
print("已关闭灵动岛显示")
}
}
}

4.数据同步

通过

ActivityConfiguration(for: DJDynamicIslandAttributes.self) { context in
}

方法创建实时活动视图的时候,回调的参数context类型是ActivityViewContext,可以通过context.state取到动态化数据的属性:

struct LockScreenLiveActivityView: View {
let context: ActivityViewContext<DJDynamicIslandAttributes> var body: some View {
VStack {
Spacer(minLength: 10)
LockScreenLiveActivityStoreHeaderView(imageURL: context.state.logo, title: context.state.title, subTitle: context.state.subTitle)
Spacer(minLength: 0)
LockScreenLiveActivityProgressView(progress: context.state.progress)
Spacer(minLength: 10)
}
}
}

利用这些属性刷新视图

使用推送通知更新实时活动

前面已经介绍过,实时活动可以通过推送通知来更新数据展示,下面来介绍具体做法以及开发过程中遇到的坑

ActivityKit 提供了从应用程序启动、更新和结束实时活动的功能。我们可以使用Token通过从服务器发送到 Apple 推送通知服务 (APNs) 的 ActivityKit 推送通知来更新实时活动, 苹果WWDC:《Update Live Activities with push notifications》教程视频

要使用 ActivityKit 推送通知更新实时活动:

1.获取APP的推送Token

使用 ActivityKit ,在启动实时活动时获取实时活动的唯一推送Token。

func startActivity(orderId:String) throws {
let attributes = DJDynamicIslandAttributes(
// 静态数据
)
let initialContentState = DJDynamicIslandAttributes.ContentState(
// 动态数据
)
let activity = try Activity.request(
attributes: attributes,
content: .init(state: initialContentState, staleDate: nil),
pushType: .token) Task {
// 获取实时活动的唯一推送Token
for await data in activity.pushTokenUpdates {
let token = data.map { String(format: "%02x", $0) }.joined()
}
}
}

使用Activity.request方法时注意传入pushType参数为.token,指定实时活动更新方式为“基于token”的推送更新,这个token就标识了是哪部手机的哪个实时活动来接受推送通知。拿到token后,前端要把它发送给后端服务器,由后端处理发给苹果进行推送

坑2:

Activity.request方法后,token不会立刻生成,而是会异步生成,过一段时间才能取到,所以要建一个Task使用for await方式来获取

坑3:

只有真机调试才能获取token,模拟器无法生成token(苹果APNs不会为模拟器下发推送通知)

2.为APP开启推送通知能力

在苹果开发者中心developer.apple.com 申请一个用于通知的key

之后可以获得:

一个10个字符的Key ID,后续的推送中会用到

一个authentication token signing key,是一个.p8类型的文件,后续的推送中需要传入它的存储路径。

3.将要推送的数据进行封装,准备进行通知推送

"aps": {
"timestamp":'$(date +%s)',
"event":"update",
"content-state":{
"logo": "https://img.duoziwang.com/2016/12/17/16485364877.jpg",
"title": "订单已经开始配送",
"subTitle": "快递员正在加急配送",
"progress": 0.6
}
}

aps内的数据就是推送通知内容,timestamp是时间戳;event是通知类型,分为update和end两种;content-state就是上文中定义的ActivityAttributes动态数据属性部分,这里的key要与属性名对应,接到通知后就可以自动解析并更新数据

坑4:

所有的属性,在content-state里都要有对应的key-value,就算是空的也要写上,不然会解析失败

4.编写服务器脚本

上面封装好的数据,要由后端服务器负责发送给苹果推送服务器(APNs),这个过程就要用到之前几步拿到的信息。这里我把推送脚本的模版提供给大家,大家可以在这个基础上进行修改:

#!/bin/bash

# Set and export your shell variables
export TEAM_ID="苹果开发者账号的teamID"
export TOKEN_KEY_FILE_NAME="第二步拿到的.p8文件存储路径"
export AUTH_KEY_ID="第二步拿到的Key ID"
export TOPIC="app的BundleIdentifier.push-type.liveactivity"
export ACTIVITY_PUSH_TOKEN="第一步拿到的token"
export APNS_HOST_NAME="api.sandbox.push.apple.com" # Calculate JWT components
export JWT_ISSUE_TIME=$(date +%s)
export JWT_HEADER=$(printf '{ "alg": "ES256", "kid": "%s" }' "${AUTH_KEY_ID}" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
export JWT_CLAIMS=$(printf '{ "iss": "%s", "iat": %d }' "${TEAM_ID}" "${JWT_ISSUE_TIME}" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
export JWT_HEADER_CLAIMS="${JWT_HEADER}.${JWT_CLAIMS}"
export JWT_SIGNED_HEADER_CLAIMS=$(printf "${JWT_HEADER_CLAIMS}" | openssl dgst -binary -sha256 -sign "${TOKEN_KEY_FILE_NAME}" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
export AUTHENTICATION_TOKEN="${JWT_HEADER}.${JWT_CLAIMS}.${JWT_SIGNED_HEADER_CLAIMS}" # Send APNs request
curl -v --header "apns-topic: $TOPIC" \
--header "apns-push-type: liveactivity" \
--header "apns-priority: 10" \
--header "authorization: bearer $AUTHENTICATION_TOKEN" \
--data '{
"aps": {
"timestamp":'$(date +%s)',
"event":"update",
"content-state":{
#动态数据
}
}
}' \
--http2 "https://${APNS_HOST_NAME}/3/device/${ACTIVITY_PUSH_TOKEN}"

此部分请求头部信息格式来源:

Establishing a token-based connection to APNs

Sending push notifications using command-line tools

Updating Live Activities with ActivityKit push notifications

运行成功后控制台显示“HTTP/2 200”代表成功了!

更新视图:

作者:京东零售 姜海

来源:京东云开发者社区 转载请注明来源

iOS16新特性:实时活动-在锁屏界面实时更新APP消息的更多相关文章

  1. 重新想象 Windows 8.1 Store Apps (92) - 其他新特性: CoreDispatcher, 日历, 自定义锁屏系列图片

    [源码下载] 重新想象 Windows 8.1 Store Apps (92) - 其他新特性: CoreDispatcher, 日历, 自定义锁屏系列图片 作者:webabcd 介绍重新想象 Win ...

  2. win10锁屏界面无法更新

    win10的锁屏界面都是巨硬公司推送过来的,质量还不错,最近锁屏界面无法更新,解决方案如下: 以管理员身份运行cmd,分别运行如下两个命令 del /f /s /q /a "%userpro ...

  3. iOS开发--QQ音乐练习,后台播放和锁屏界面

    一.设置后台播放 首先允许程序后台播放 代码实现 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOpti ...

  4. 解决:Android4.3锁屏界面Emergency calls only - China Unicom与EMERGENCY CALL语义重复

    从图片中我们可以看到,这里在语义上有一定的重复,当然这是谷歌的原始设计.这个问题在博客上进行共享从表面上来看着实没有什么太大的意义,不过由于Android4.3在锁屏功能上比起老版本做了很大的改动,而 ...

  5. android------锁屏(手机启动出现锁屏界面)

    以前用过一个红包锁屏的软件,第一次打开手机出现锁屏,滑动领取收益,当时觉得这功能不错,就查阅资料,写了一个案例, apk运行流程: 进入软件--->启动服务--->关闭手机(可先退出应用) ...

  6. Android 7.1.1 锁屏界面启动流程

    前几天遇到一个低概率复现锁屏界面不显示,仅仅显示状态栏的问题,跟了下锁屏界面启动显示的流程,在这分享下,也方便以后自己查看.前面简介了下Zygote启动流程, Zygote进程启动后会首先创建一个Sy ...

  7. 解决:Android4.3锁屏界面Emergency calls only - China Unicom与EMERGENCY CALL语义反复

    从图片中我们能够看到,这里在语义上有一定的反复,当然这是谷歌的原始设计.这个问题在博客上进行共享从表面上来看着实没有什么太大的意义,只是因为Android4.3在锁屏功能上比起老版本号做了非常大的修改 ...

  8. Qt - 锁屏界面加虚拟小键盘

    一.实现效果 鼠标点击"密码输入栏",弹出虚拟键盘,输入锁屏密码后,点击虚拟键盘外部区域,则会隐藏虚拟键盘,再点击登录,成功进入主界面. 二.虚拟键盘-程序设计 2.1 frmNu ...

  9. RK3399 删除开机锁屏界面

    CPU:RK3399 系统:Android 7.1 删除开机锁屏界面 diff --git a/frameworks/base/packages/SettingsProvider/res/values ...

  10. iOS16新特性 | 灵动岛适配开发与到家业务场景结合的探索实践

    作者:京东零售 姜海 灵动岛是苹果在iPhone 14 Pro和iPhone 14 Pro Max上首次提出的全新UI交互形式,创新性的让虚拟软件和硬件的交互变得更为流畅.当有来电.短信等通知时,灵动 ...

随机推荐

  1. R 语言主成分分析(PCA)实战教程

    作者:落痕的寒假原文:https://blog.csdn.net/LuohenYJ/article/details/97950522 声明:本文章经原作者同意后授权转载. 主成分分析 Principa ...

  2. Basic Pentesting

    来自tryhackme的 Basic Pentesting 开靶场IP:10.10.227.255 # nmap 端口扫描 PORT STATE SERVICE VERSION 22/tcp open ...

  3. 01-C语言基础语法

    目录 一. C语言发展史 二. 数据类型 三. 常量和变量 四. 字符串和转义字符 五. 选择语句 六. 循环语句 七. 函数 一. C语言发展史 1963 年ALGOL 60 作为C语言最早的模型, ...

  4. 如何取消Blazor Server烦人的重新连接?

    如何取消Blazor Server烦人的重新连接? 相信很多Blazor的用户在开发内部系统上基本上都选择速度更快,加载更快的Blazor Server模式. 但是Blazor Server由于是Si ...

  5. mysql索引优化-01

    1.1索引是什么?   mysql官方对于索引的定义:可以帮助mysql高效的获取数据的数据结构.   mysql在存储数据之外,数据库系统中还维护着满足特定查找算法的数据结构,这些数据结构给以某种引 ...

  6. Mininet教程

    mininet的安装 1.前言 1.本次安装环境为ubuntu20.04. 2.mininet 为 github上的最新版,我已经修改镜像地址并克隆到了gitee,只需要从我的gitee仓库克隆即可. ...

  7. 从硅谷到北京,百位AI大咖连续两天集聚讨论AI智能和实践

    全球AI大咖齐聚北京,探讨人工智能前沿!百位AI大咖倾力出席,冲向AI大浪潮! AI从业者和企业家们,一场引领未来的科技盛宴即将在北京掀起!我们荣幸地宣布,第四届"数据智能创新与实践人工智能 ...

  8. Day01_Java作业

    A:选择题 1:下列标识符哪个是合法的(a) A.class B.$abc C.1234 D.Car.taxi B:填空题 1: java源程序的扩展名是( .java ) 2: java程序经编译后 ...

  9. Microsoft edge锁定在任务栏上,被修改主页360的解决方法

    今天从桌面下边的任务栏打开Microsoft edge浏览器,突然发现主页被篡改为360导航了(生气!恶龙咆哮ooo 在桌面上是Microsoft edge,固定到任务栏就成为Microsoft ed ...

  10. 订单逆向履约系统的建模与 PaaS 化落地实践

    导读 本文重点介绍了京东零售电商业务在订单逆向履约上面的最佳技术实践,京东零售快退平台承接了零售几乎所有售前逆向拦截和退款业务,并在长期的业务和技术探索中沉淀了丰富的业务场景设计方案.架构设计经验,既 ...