写点什么

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

  • 2023-09-18
    北京
  • 本文字数:5900 字

    阅读完需:约 19 分钟

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<ActivityAttributes>,可以通过 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 variablesexport 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 componentsexport 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 requestcurl -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”代表成功了!



更新视图:



作者:京东零售 姜海

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

发布于: 刚刚阅读数: 4
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
iOS16新特性:实时活动-在锁屏界面实时更新APP消息 | 京东云技术团队_iOS16_京东科技开发者_InfoQ写作社区