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: 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消息的更多相关文章
- 重新想象 Windows 8.1 Store Apps (92) - 其他新特性: CoreDispatcher, 日历, 自定义锁屏系列图片
		
[源码下载] 重新想象 Windows 8.1 Store Apps (92) - 其他新特性: CoreDispatcher, 日历, 自定义锁屏系列图片 作者:webabcd 介绍重新想象 Win ...
 - win10锁屏界面无法更新
		
win10的锁屏界面都是巨硬公司推送过来的,质量还不错,最近锁屏界面无法更新,解决方案如下: 以管理员身份运行cmd,分别运行如下两个命令 del /f /s /q /a "%userpro ...
 - iOS开发--QQ音乐练习,后台播放和锁屏界面
		
一.设置后台播放 首先允许程序后台播放 代码实现 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOpti ...
 - 解决:Android4.3锁屏界面Emergency calls only - China Unicom与EMERGENCY CALL语义重复
		
从图片中我们可以看到,这里在语义上有一定的重复,当然这是谷歌的原始设计.这个问题在博客上进行共享从表面上来看着实没有什么太大的意义,不过由于Android4.3在锁屏功能上比起老版本做了很大的改动,而 ...
 - android------锁屏(手机启动出现锁屏界面)
		
以前用过一个红包锁屏的软件,第一次打开手机出现锁屏,滑动领取收益,当时觉得这功能不错,就查阅资料,写了一个案例, apk运行流程: 进入软件--->启动服务--->关闭手机(可先退出应用) ...
 - Android 7.1.1 锁屏界面启动流程
		
前几天遇到一个低概率复现锁屏界面不显示,仅仅显示状态栏的问题,跟了下锁屏界面启动显示的流程,在这分享下,也方便以后自己查看.前面简介了下Zygote启动流程, Zygote进程启动后会首先创建一个Sy ...
 - 解决:Android4.3锁屏界面Emergency calls only - China Unicom与EMERGENCY CALL语义反复
		
从图片中我们能够看到,这里在语义上有一定的反复,当然这是谷歌的原始设计.这个问题在博客上进行共享从表面上来看着实没有什么太大的意义,只是因为Android4.3在锁屏功能上比起老版本号做了非常大的修改 ...
 - Qt - 锁屏界面加虚拟小键盘
		
一.实现效果 鼠标点击"密码输入栏",弹出虚拟键盘,输入锁屏密码后,点击虚拟键盘外部区域,则会隐藏虚拟键盘,再点击登录,成功进入主界面. 二.虚拟键盘-程序设计 2.1 frmNu ...
 - RK3399 删除开机锁屏界面
		
CPU:RK3399 系统:Android 7.1 删除开机锁屏界面 diff --git a/frameworks/base/packages/SettingsProvider/res/values ...
 - iOS16新特性 | 灵动岛适配开发与到家业务场景结合的探索实践
		
作者:京东零售 姜海 灵动岛是苹果在iPhone 14 Pro和iPhone 14 Pro Max上首次提出的全新UI交互形式,创新性的让虚拟软件和硬件的交互变得更为流畅.当有来电.短信等通知时,灵动 ...
 
随机推荐
- kafka集群是如何选择leader,你知道吗?
			
前言 kafka集群是由多个broker节点组成,这里面包含了许多的知识点,以下的这些问题你都知道吗? 你知道topic的分区leader是怎么选举的吗? 你知道zookeeper中存储了kafka的 ...
 - C2简介
			
C2简介 学习命令与控制的基本知识,帮助您成为更好的红队队员并简化您的下一次红队评估! thm:https://tryhackme.com/room/introtoc2 介绍 命令与控制 ( C2 ) ...
 - Python爬虫————泉州二手房数据爬取和数据可视化
			
1.选题的背景 我本次的主题是泉州二手房房价的调查分析. 首先通过爬虫采集链家网上所有二手房的房源数据,并对采集到的数据进行清洗:然后,对清洗后的数据进行可视化分析,探索隐藏在大量数据背后的规律:将这 ...
 - 如何从零开始构建 API ?
			
假设你请承包商从零开始建造一座房子,你肯定期望他们交付最高质量的房子.他们必须通过检查.遵守安全规范并遵循项目中约定的要求.因为建房子可容不得走捷径.如果承包商经常走捷径,他们的声誉会受到影响,从而失 ...
 - 我借助 AI 神器,快速学习《阿里的 Java 开发手册》,比量子力学还夸张
			
我平时经常要看 PDF,但是我看书贼慢,一个 PDF 差不多几十上百页,看一遍要花挺长时间. 我记性还不好,看完之后,过些日子就记不清 PDF 是讲什么的了.为了找到 PDF 里的某些信息,又得再花时 ...
 - 手把手教你实战TDD
			
1. 前言 领域驱动设计,测试驱动开发. 我们在<手把手教你落地DDD>一文中介绍了领域驱动设计(DDD)的落地实战,本文将对测试驱动开发(TDD)进行探讨,主要内容有:TDD基本理解.T ...
 - ChatGPT使用案例,助你快速上手,做事事半功倍
			
ChatGPT介绍 首先我们来看一下chat-gpt自己的介绍: ChatGPT的发展历程 2015年,OpenAI成立,致力于研究和开发人工智能技术.在成立初期,OpenAI的创始人之一Elon M ...
 - Git使用教程(带你玩转GitHub)
			
Git使用教程(理论实体结合体系版) 下载安装: 按照这个博客来就好 Windows系统Git安装教程(详解Git安装过程) - 学为所用 - 博客园 (cnblogs.com) Git命令大全: G ...
 - Java 统计大串中小串出现的次数 举例:在字符串"woaijavawozhenaijavawozhendeaijavawozhendehenaijavaxinbuxinwoaijavagun"中java出现了5次
			
代码如下: public static void main(String[] args) { //大串 String max = "woaijavawozhenaijavawozhendea ...
 - Visual Studio Code调试和发布ASP.NET Core Web应用
			
前言 上一篇文章主要讲了Visual Studio Code安装C#开发工具包并编写ASP.NET Core Web应用有兴趣的同学可以去看看,今天咱们主要是要讲讲如何在VS Code中调试和发布AS ...