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

news2024/11/25 7:21:35

简介

之前在 《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”代表成功了!

更新视图:

作者:京东零售 姜海

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1024189.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Verilog教程】2.3 Verilog 数据类型

Verilog 最常用的 2 种数据类型就是线网&#xff08;wire&#xff09;与寄存器&#xff08;reg&#xff09;&#xff0c;其余类型可以理解为这两种数据类型的扩展或辅助。 线网&#xff08;wire&#xff09; wire 类型表示硬件单元之间的物理连线&#xff0c;由其连接的器件输…

脑网络图谱

前言 研究人脑面临的一个挑战是其多尺度组织和系统复杂性。我们对大脑组织的认识主要来源于离体组织学检查&#xff0c;如细胞结构映射。通过研究全脑微观结构特征的变化&#xff0c;可以划分为不同的脑区。然而&#xff0c;这种研究大脑组织的“局部”方法非常耗时、耗资源&a…

Python 图形化界面基础篇:创建工具栏

Python 图形化界面基础篇&#xff1a;创建工具栏 引言 Tkinter 库简介步骤1&#xff1a;导入 Tkinter 模块步骤2&#xff1a;创建 Tkinter 窗口步骤3&#xff1a;创建工具栏步骤4&#xff1a;向工具栏添加工具按钮步骤5&#xff1a;处理工具按钮的点击事件步骤6&#xff1a;启动…

window10下安装docker教程

要在Windows 10上安装Docker&#xff0c;您可以按照以下步骤进行操作&#xff1a; 在您的Web浏览器中&#xff0c;访问Docker官方网站&#xff1a;https://www.docker.com/get-started。然后&#xff0c;点击"Download Docker Desktop"按钮。 在下载页面上&#xff…

ruoyi-vue-pro 项目安装使用过程中的问题解决

ruoyi-vue-pro是功能比较多的一款前后端平台,因为刚接触ruoyi平台不久&#xff0c;为了更好了解ruoyi平台的相关功能&#xff0c;就本地部署了&#xff0c;作者主动屏蔽了部分功能&#xff0c;部分功能会提示功能能未启用&#xff0c;同时启用dev环境的时候是演示版本&#xff…

使用香橙派学习Linux udev的rules 并实现U盘的自动挂载

在之前编程首先语音刷抖音的博文里提到过udev&#xff0c;现在回顾一下&#xff1a; 什么是udev&#xff1f; udev是一个设备管理工具&#xff0c;udev以守护进程的形式运行&#xff0c;通过侦听内核发出来的uevent来管理/dev目录下的设备文件。udev在用户空间运行&#xff0c;…

SQL Server关于AlwaysOn的理解-读写分离的误区(一)

前言 很多人认为AlwaysOn在同步提交模式下数据是实时同步的&#xff0c;也就是说在主副本写入数据后可以在辅助副本立即查询到。因此期望实现一个彻底的读写分离策略&#xff0c;即所有的写语句在主副本上&#xff0c;所有的只读语句分离到辅助副本上。这是一个认知误区&#x…

jmeter基础压力教程

Jmeter基础压力测试教程 一、安装Jmeter&#xff1b; 安装需求&#xff1a;1. JDK 8.0.91安装包&#xff08;最新即可&#xff0c;配置环境变量&#xff09; 2. Badboy2.25脚本录制工具&#xff08;注&#xff1a;Jmeter3.0与badboy2.0不兼容&#xff09; Jmerter安装包…

玩玩“小藤”开发者套件 Atlas 200I DK A2 之环境准备

玩玩“小藤”开发者套件 Atlas 200I DK A2 之环境准备 0. 背景1. 烧录镜像2. 安装依赖3. 安装 Ascend-cann-toolkit 软件包4. 安装PyTorch5. 安装 PyTorch 插件 torch_npu6. 安装APEX混合精度模块 0. 背景 总所周知&#xff0c;英伟达的GPU供不应求&#xff0c;还各种限制。华…

Ae 效果:CC Bubbles

模拟/CC Bubbles Simulation/CC Bubbles CC Bubbles&#xff08;CC 气泡&#xff09;主要用于生成气泡以及模拟其运动的效果。 既可将效果应用到一个纯色图层&#xff0c;也可将其应用到一个图层的副本上或者调整图层上。CC Bubbles 效果生成的气泡在运动过程会与图层内容即时发…

现货白银的价格如何变动

只要是在正常的交易日&#xff0c;现货白银价格的走势&#xff0c;几乎全天24小时都处于波动之中&#xff0c;其行情走势会因为各种政治、经济因素的影响而发生改变&#xff0c;有时价格波动在一天内可以高达两三美元&#xff0c;有时却可以连续几周都处于窄幅波动&#xff0c;…

详解Nacos和Eureka的区别

文章目录 Eureka是什么Nacos是什么Nacos的实现原理 Nacos和Eureka的区别CAP理论连接方式服务异常剔除操作实例方式自我保护机制 Eureka是什么 Eureka 是Spring Cloud 微服务框架默认的也是推荐的服务注册中心, 由Netflix公司与2012将其开源出来,Eureka基于REST服务开发,主要用…

爬虫项目(四):抓取网页所有图片

文章目录 一、书籍推荐二、完整代码三、运行结果 一、书籍推荐 推荐本人书籍《Python网络爬虫入门到实战》 &#xff0c;详细介绍见&#x1f449;&#xff1a; 《Python网络爬虫入门到实战》 书籍介绍 二、完整代码 原理&#xff1a;抓取该链接中所有的图片格式。基于seleni…

分布式数据库(笔记)

课程链接&#xff1a;015_数据库系统的用户接口以及SQL语言&#xff08;一&#xff09;_哔哩哔哩_bilibili 1、分类 DDB&#xff1a;物理上分散&#xff0c;逻辑上集中 特点&#xff1a; 有统一的全局模式靠分布式数据库管理系统实现 DDBMS分布在网络的不同节点上 优点&am…

Responder

环境准备 操作系统:Kali Linux工具:responder,john,evil-winrm PS:输入以下命令解决靶场环境无法打开问题 #echo "<靶机IP> unika.htb">>/etc/hostsresponder工具 [Kali 官网] 手册地址:https://www.kali.org/tools/responder/ 摘要: This package c…

ubuntu 里根文件系统的扩容,/dev/ubuntu-vg/ubuntu-lv 文件系统扩充到整个分区

笔者安装了ubuntu服务器版软件&#xff0c;由于系统安装的时候没有划分好磁盘分区&#xff0c;只采用了1000G固态硬盘的 200G来安装系统&#xff0c;安装完毕后&#xff0c;用df -h 命令查看如下&#xff1a; 根文件系统仅占用了 196G&#xff0c;而本身硬盘的尺寸为1000G&…

C++笔记之std::forward

C笔记之std::forward 文章目录 C笔记之std::forward例一例二 std::forward的作用是在C中帮助实现完美转发&#xff08;perfect forwarding&#xff09;&#xff0c;它将传递给它的参数以原始类型和引用的方式传递给下一个函数&#xff0c;保持参数的值类别&#xff08;lvalue或…

爬虫 — Js 逆向

目录 一、概念1、爬虫2、反爬虫3、加密解密4、加密5、步骤 二、常用加密方式1、加密方式2、常见加密算法3、JS 中常见的算法4、MD5 算法4.1、MD5 加密网站4.2、特点 5、DES/AES 算法6、RSA 算法7、base64 算法 三、环境配置1、node.js 环境配置2、PyCharm 环境配置 一、概念 1…

【AI视野·今日NLP 自然语言处理论文速览 第三十六期】Tue, 19 Sep 2023

AI视野今日CS.NLP 自然语言处理论文速览 Tue, 19 Sep 2023 (showing first 100 of 106 entries) Totally 106 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Computation and Language Papers Speaker attribution in German parliamentary debates with QLoRA-ada…

xp 系统 安装 python 2.7 ide pip

1 下载python http://www.python.org/ftp/python/ python-2.7.2.msi 安装完需要设置环境变量 2 下载 setuptools setuptools-0.6c11.win32-py2.7.exe https://pypi.tuna.tsinghua.edu.cn/simple/setuptools/ 3 下载 pip &#xff0c;python 2.7 最高支持 pip 20.3.4 https:…