当前位置: 首页 > news >正文

iOS16新特性:实时活动-在锁屏界面实时更新APP消息 | 京东云技术团队

简介

之前在 《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: Stringlet title: Stringlet subTitle: Stringvar 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: CGFloatlet borderOffset = 20.0var 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 = ContentStatepublic struct ContentState: Codable, Hashable {// 动态数据var logo: String = ""var title: String = ""var subTitle: String = ""var progress: Double = 0}// 静态数据var totalAmount: Stringvar 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 {// 获取实时活动的唯一推送Tokenfor 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消息 | 京东云技术团队

简介 之前在 《iOS16新特性:灵动岛适配开发与到家业务场景结合的探索实践》 里介绍了iOS16新的特性&#xff1a;实时更新&#xff08;Live Activity&#xff09;中灵动岛的适配流程&#xff0c;但其实除了灵动岛的展示样式&#xff0c;Live Activity还有一种非常实用的应用场景…...

使用 Elasticsearch、OpenAI 和 LangChain 进行语义搜索

在本教程中&#xff0c;我将引导您使用 Elasticsearch、OpenAI、LangChain 和 FastAPI 构建语义搜索服务。 LangChain 是这个领域的新酷孩子。 它是一个旨在帮助你与大型语言模型 (LLM) 交互的库。 LangChain 简化了与 LLMs 相关的许多日常任务&#xff0c;例如从文档中提取文本…...

NIFI集群_队列Queue中数据无法清空_清除队列数据报错_无法删除queue_解决_集群中机器交替重启删除---大数据之Nifi工作笔记0061

今天发现,有两个处理器,启动以后,数据流不过去,后来,锁定问题在,queue队列上面,因为别的队列都可以通过,右键,empty queue清空,就是 这个队列不行,这个队列无法被删除,至于为什么导致这样的, 猜测是因为之前,流程设计好以后,队列没有设置背压,也没有设置队列中的内容大小和fl…...

leetcode20. 有效的括号 [简单题]

题目 给定一个只包括 (&#xff0c;)&#xff0c;{&#xff0c;}&#xff0c;[&#xff0c;] 的字符串 s &#xff0c;判断字符串是否有效。 有效字符串需满足&#xff1a; 左括号必须用相同类型的右括号闭合。左括号必须以正确的顺序闭合。每个右括号都有一个对应的相同类型…...

ubuntu20.04下源码编译colmap

由于稠密重建需要CUDA&#xff0c;因此先安装CUDA&#xff0c;我使用的是3050GPU&#xff0c;nvidia-smi显示最高支持CUDA11.4。 不要用sudo apt安装&#xff0c;版本较低&#xff0c;30系显卡建议安装CUDA11.0以上&#xff0c;这里安装了11.1版本。 下载&#xff1a; cuda_1…...

Jumpserver堡垒机

一、堡垒机概述 1、堡垒机的基本概念 堡垒机也是一台服务器&#xff0c;在一个特定的网络环境下&#xff0c;为了保障网络和数据不受来自外部和内部用户的入侵和破坏&#xff0c;而运用各种技术手段实时收集、监控网络环境中每一个组成部分&#xff08;服务器&#xff09;的系…...

第一百五十三回 如何实现滑动窗口

文章目录 概念介绍实现方法示例代码 我们在上一章回中介绍了自定义组件实现游戏摇杆相关的内容&#xff0c;本章回中将介绍 如何实现滑动窗口.闲话休提&#xff0c;让我们一起Talk Flutter吧。 概念介绍 我们在本章回中介绍的滑动窗口表示在屏幕底部向上滑动时弹出一个窗口&a…...

Oracle 12c自动化管理特性的新进展:自动备份、自动恢复和自动维护功能的优势|oracle 12c相对oralce 11g的新特性(3)

一、前言: 前面几期讲解了oracle 12c多租户的使用、In-Memory列存储来提高查询性能以及数据库的克隆、全局数据字典和共享数据库资源的使用 今天我们讲讲oracle 12c的另外的一个自动化管理功能新特性:自动备份、自动恢复、自动维护的功能 二、自动备份、自动恢复、自动维护…...

Redis——Jedis中hash类型使用

hset 和 hget hset可以逐一添加key和value&#xff0c;也可以通过map类型来直接添加多组fields 而hget则返回string类型&#xff0c;如果元素不存在则返回null private static void hsetAndHget(Jedis jedis) {jedis.flushAll();jedis.hset("key", "f1"…...

肖sir__项目实战讲解__004

项目实战讲解 一、项目的类型 金融类&#xff1a; 保险(健康险理财险)、证券、基金(股票型基金、混合型基金、指数型基金、债券型基金、 天天基金网&#xff08;ETF基金、货币型基金、量化基金)、银行、贷款、信用卡、外汇、二元期权、期货原油、blockchain、 数字货币、黄金白…...

数据库数据恢复-ORACLE常见故障有哪些?恢复数据的可能性高吗?

ORACLE数据库常见故障&#xff1a; 1、ORACLE数据库无法启动或无法正常工作。 2、ORACLE数据库ASM存储破坏。 3、ORACLE数据库数据文件丢失。 4、ORACLE数据库数据文件部分损坏。 5、ORACLE数据库DUMP文件损坏。 ORACLE数据库数据恢复可能性分析&#xff1a; 1、ORACLE数据库无…...

合规性管理如何帮助产品团队按时交付?

成功的产品和产品发布背后通常需要经过一个涉及多个监督机构、多功能团队和利益相关者的复杂流程。在组织的治理、风险管理和合规性&#xff08;GRC&#xff09;框架下&#xff0c;产品团队不仅需要追求市场创新&#xff0c;还需要确保符合所有适用的法规、标准和合同要求。由于…...

从平均数到排名算法

平均数用更少的数字&#xff0c;概括一组数字。属于概述统计量、集中趋势测度、位置测度。中位数是第二常见的概述统计量。许多情况下比均值更合适。算术平均数是3中毕达哥拉斯平均数之一&#xff0c;另外两种毕达哥拉斯平均数是几何平均数和调和平均数。 算术平均 A M 1 n ∑…...

如何使用ESP8266微控制器和Nextion显示器为Home Assistant展示温度传感器和互联网天气预报

第一部分&#xff1a;引言与项目概述 在智能家居领域&#xff0c;实时监控和显示环境数据已经成为了一个热门的话题。无论是室内温度、室外温度&#xff0c;还是游泳池的温度&#xff0c;都可以通过各种传感器轻松获取。但如何将这些数据以直观、美观的方式展现出来呢&#xf…...

阻塞队列-生产者消费者模型

阻塞队列介绍标准库阻塞队列使用基于阻塞队列的简单生产者消费者模型。实现一个简单型阻塞队列 &#xff08;基于数组实现&#xff09; 阻塞队列介绍 不要和之前学多线程的就绪队列搞混&#xff1b; 阻塞队列&#xff1a;也是一个队列&#xff0c;先进先出。带有特殊的功能 &…...

Vector Art - 矢量艺术

什么是矢量艺术&#xff1f; 矢量图形允许创意人员构建高质量的艺术作品&#xff0c;具有干净的线条和形状&#xff0c;可以缩放到任何大小。探索这种文件格式如何为各种规模的项目提供创造性的机会。 什么是矢量艺术作品? 矢量艺术是由矢量图形组成的艺术。这些图形是基于…...

ruoyi-nbcio增加flowable流程待办消息的提醒,并提供右上角的红字数字提醒(一)

更多ruoyi-nbcio功能请看演示系统 gitee源代码地址 前后端代码&#xff1a; https://gitee.com/nbacheng/ruoyi-nbcio 演示地址&#xff1a;RuoYi-Nbcio后台管理系统 1、数据库表方面 在原来sys_notice修改基础上增加一个表叫sys_notice_send 表结构如下&#xff1a; DROP …...

数据结构:二叉树的基本概念

文章目录 1. 二叉树的定义2. 二叉树的特点3. 特殊二叉树斜树满二叉树完全二叉树 4. 二叉树的性质 1. 二叉树的定义 如果我们猜一个100以内的数字,该怎么猜才能理论最快呢? 第一种方式:从1,2一直猜到100, 反正数字都是100以内,总能猜到的 第二种方式:先猜50,如果比结果小,猜75…...

利用Socks5代理IP加强跨界电商爬虫的网络安全

随着跨界电商的兴起&#xff0c;爬虫技术在这个领域变得越来越重要。然而&#xff0c;网络安全一直是一个值得关注的问题。在本文中&#xff0c;我们将讨论如何利用代理IP和Socks5代理来增强跨界电商爬虫的网络安全&#xff0c;确保稳定和可靠的数据采集&#xff0c;同时避免封…...

Spring学习笔记6 Bean的实例化方式

Spring学习笔记5 GoF之工厂模式_biubiubiu0706的博客-CSDN博客 Spring为Bean提供了多种实例化方式,通常包括4中(目的:更加灵活) 1.通过构造方法实例化 2.通过简单工厂模式实例化 3.通过factory-bean实例化 4.通过FactoryBean接口实例化 新建模块 spring-005 依赖 <!--S…...

第19节 Node.js Express 框架

Express 是一个为Node.js设计的web开发框架&#xff0c;它基于nodejs平台。 Express 简介 Express是一个简洁而灵活的node.js Web应用框架, 提供了一系列强大特性帮助你创建各种Web应用&#xff0c;和丰富的HTTP工具。 使用Express可以快速地搭建一个完整功能的网站。 Expre…...

阿里云ACP云计算备考笔记 (5)——弹性伸缩

目录 第一章 概述 第二章 弹性伸缩简介 1、弹性伸缩 2、垂直伸缩 3、优势 4、应用场景 ① 无规律的业务量波动 ② 有规律的业务量波动 ③ 无明显业务量波动 ④ 混合型业务 ⑤ 消息通知 ⑥ 生命周期挂钩 ⑦ 自定义方式 ⑧ 滚的升级 5、使用限制 第三章 主要定义 …...

Qt Widget类解析与代码注释

#include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this); }Widget::~Widget() {delete ui; }//解释这串代码&#xff0c;写上注释 当然可以&#xff01;这段代码是 Qt …...

DAY 47

三、通道注意力 3.1 通道注意力的定义 # 新增&#xff1a;通道注意力模块&#xff08;SE模块&#xff09; class ChannelAttention(nn.Module):"""通道注意力模块(Squeeze-and-Excitation)"""def __init__(self, in_channels, reduction_rat…...

Module Federation 和 Native Federation 的比较

前言 Module Federation 是 Webpack 5 引入的微前端架构方案&#xff0c;允许不同独立构建的应用在运行时动态共享模块。 Native Federation 是 Angular 官方基于 Module Federation 理念实现的专为 Angular 优化的微前端方案。 概念解析 Module Federation (模块联邦) Modul…...

DBAPI如何优雅的获取单条数据

API如何优雅的获取单条数据 案例一 对于查询类API&#xff0c;查询的是单条数据&#xff0c;比如根据主键ID查询用户信息&#xff0c;sql如下&#xff1a; select id, name, age from user where id #{id}API默认返回的数据格式是多条的&#xff0c;如下&#xff1a; {&qu…...

优选算法第十二讲:队列 + 宽搜 优先级队列

优选算法第十二讲&#xff1a;队列 宽搜 && 优先级队列 1.N叉树的层序遍历2.二叉树的锯齿型层序遍历3.二叉树最大宽度4.在每个树行中找最大值5.优先级队列 -- 最后一块石头的重量6.数据流中的第K大元素7.前K个高频单词8.数据流的中位数 1.N叉树的层序遍历 2.二叉树的锯…...

稳定币的深度剖析与展望

一、引言 在当今数字化浪潮席卷全球的时代&#xff0c;加密货币作为一种新兴的金融现象&#xff0c;正以前所未有的速度改变着我们对传统货币和金融体系的认知。然而&#xff0c;加密货币市场的高度波动性却成为了其广泛应用和普及的一大障碍。在这样的背景下&#xff0c;稳定…...

大语言模型(LLM)中的KV缓存压缩与动态稀疏注意力机制设计

随着大语言模型&#xff08;LLM&#xff09;参数规模的增长&#xff0c;推理阶段的内存占用和计算复杂度成为核心挑战。传统注意力机制的计算复杂度随序列长度呈二次方增长&#xff0c;而KV缓存的内存消耗可能高达数十GB&#xff08;例如Llama2-7B处理100K token时需50GB内存&a…...

CSS设置元素的宽度根据其内容自动调整

width: fit-content 是 CSS 中的一个属性值&#xff0c;用于设置元素的宽度根据其内容自动调整&#xff0c;确保宽度刚好容纳内容而不会超出。 效果对比 默认情况&#xff08;width: auto&#xff09;&#xff1a; 块级元素&#xff08;如 <div>&#xff09;会占满父容器…...