iOS 灵动岛Dynamic Island实现+模拟器测试远程推送

灵动岛展示.jpeg

[阅读难度:简单]

准备工作

技术储备:

需对Xcode、Swift、SwiftUI 有一定了解

环境要求:

iOS >= 16.1
Xcode >= 14.1
设备(iPhone14Pro/iPhone14ProMax,模拟器也可以)

灵动岛简介

一句话总结:“灵动岛是展示在锁屏界面与主屏幕状态栏的“特殊”小组件

灵动岛的状态

灵动岛不同状态.png

默认状态:可展示左右两个小视图(LeadingSideTrailingSide
展开状态:用户长按灵动岛的展开状态(LeadingCenterTrailingBottom),
紧凑状态:当有多个APP灵动岛时展示分离状态,依创建顺序展示(MinimalDetached)

灵动岛的生命周期:

灵动岛主要分为StartUpdateEnd三种状态,可由ActivityKit远程推送控制其状态。
一个灵动岛默认展示8小时,结束后可继续在锁屏界面存在4个小时后由系统彻底移除,也可由开发者自行移除。

那么,我们开始吧·····


接入Widget Extension

  1. info.plist 文件中添加 NSSupportsLiveActivities 字段,设置为YES

  2. 选中项目添加target,创建Widget Extension,假设将其命名为“DynamicDemo”:

    target.png

  3. 在Widget中,主要关注WidgetsExtensionBundle,该结构体下的body返回所有小组件的Widget,Xcode 会默认生成小组件卡片的初始代码,不需要的话可以移除。

@main
struct WidgetsExtensionBundle: WidgetBundle {
    var body: some Widget {
//        DynamicDemo()     //原本的小组件卡片Widget
        DynamicDemoLiveActivity() // 灵动岛Widget
    }
}

Tips:模拟器下偶现APP动态小组件权限被关闭,从“设置-对应APP-实时活动”打开即可

2. 配置灵动岛Widget

Xcode14.1及之后的版本引入 WidgetsExtension 后,会默认创建XXXXXXLiveActivity.swift文件:


struct DynamicDemoAttributes: ActivityAttributes {
    public struct ContentState: Codable, Hashable {
        // Dynamic stateful properties about your activity go here!
        var value: Int
    }

    // Fixed non-changing properties about your activity go here!
    var name: String
}

struct DynamicDemoLiveActivity: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: DynamicDemoAttributes.self) { context in
            // Lock screen/banner UI goes here
            VStack {
                Text("Hello")
            }
            .activityBackgroundTint(Color.cyan)
            .activitySystemActionForegroundColor(Color.black)
            
        } dynamicIsland: { context in
            DynamicIsland {
                // Expanded UI goes here.  Compose the expanded UI through
                // various regions, like leading/trailing/center/bottom
                DynamicIslandExpandedRegion(.leading) {
                    Text("Leading")
                }
                DynamicIslandExpandedRegion(.trailing) {
                    Text("Trailing")
                }
                DynamicIslandExpandedRegion(.bottom) {
                    Text("Bottom")
                    // more content
                }
            } compactLeading: {
                Text("L")
            } compactTrailing: {
                Text("T")
            } minimal: {
                Text("Min")
            }
            .widgetURL(URL(string: "http://www.apple.com"))
            .keylineTint(Color.red)
        }
    }
}

DynamicDemoLiveActivity: 灵动岛的Widget,通过ActivityConfiguration来配置所有状态下的视图,包括不支持灵动岛设备的设备在锁屏页面的视图。
DynamicDemoAttributes: 灵动岛属性属性结构体,用于数据更新,由对应的Activity使用。

3. 控制灵动岛

在需要控制灵动岛的地方,引入ActivityKit。针对“Start”、“Update”、“End”三种状态分别举个简单的例子:

开启:

        Task{
            // 配置Attributes
            let data = DynamicDemoAttributes(value: 100)
            let state = DynamicDemoAttributes.ContentState()
            // 根据 Attributes 开启一个灵动岛
            do {
                // reqeust 指定 pushType 为 .token 可获取远程推送所需的 token
                try await MainActor.run {
                    let activity = try Activity<DynamicDemoAttributes>.request(attributes: data, contentState: state)
                }
            } catch (let error) {
                print(error.localizedDescription)
            }
        }

更新:

            do {
                let data = DynamicDemoAttributes(value:100)
                let state = DynamicDemoAttributes.ContentState(progressMsg: "更新的文案")
                
                if let activity = Activity<DynamicDemoAttributes>.activities.first {
                    await activity.update(using: state)
                } else {
                    try Activity<DynamicDemoAttributes>.request(attributes: data, contentState: state)
                }
            } catch (let error) {
                print(error.localizedDescription)
            }

代码中取的 activities.first,实际开发中应该根据数据来区分并获取需要更新的灵动岛实例。

通过获取当前需要更新的灵动岛实例,通过调用update方法传入最新的Attributes更新。

Tips: 灵动岛可由宿主APP与远程推送更新,普通APP会在退到后台后10s内被挂起,例如音乐、健身、定位、通话等可常驻后台的应用可直接通过宿主APP及时更新。对于退出APP后需要更新的场景,可由远程推送实现。具体可参考文末。

结束:


            //移除所有
            Activity<DynamicDemoAttributes>.activities.forEach { item in
                Task{
//                    await item.end() //默认结束后,会在锁屏界面等待4小时彻底移除
                    await item.end(dismissalPolicy:.immediate) // 立即结束
                }
            }

Tips: 指定.immediate可以立即移除


Q&A

Q1:如何识别用户点击的视图:

当前的灵动岛不支持非系统自定义交互(截止20221121),支持DeepLinkLink跳转。点击灵动岛默认跳转我们的宿主APP,我们可以在灵动岛视图中配置widgetURL()Link()来达到识别用户点击的效果、以Link()举例:


···············
DynamicIslandExpandedRegion(.bottom) {
                    Link(destination: URL(string: "xxxxxx://hotel/contact")!) {
                        Label("Contact Hotel", systemImage: "phone").padding()
                    }.background(Color.accentColor)
                    .clipShape(RoundedRectangle(cornerRadius: 15))
                }
···············

上面代码可以在用户点击了灵动岛的bottom视图跳转进APP时在 AppDelegateSceneDelegate 方法中识别其所带入的 url ,以此来做对应处理。

Q2:多个灵动岛展示的优先级

  1. 单个APP的多个灵动岛:默认视图(依次展示)
  2. 多个APP的灵动岛:紧凑视图/分离视图(依次展示)

Q3:如何在模拟器模拟远程推送更新

常规的推送可查看《iOS 在模拟器上测试远程推送》。在调研灵动岛的过程中,需要模拟器测试远程推送的功能。查阅一圈资料后成功实现,具体步骤如下:

环境要求

Xcode >= 14.1
MacOS >= 13.0

准备工作
如何获取远程推送的token:

        let activity = try 
        // 指定pushType为.token
        Activity<OrderProgressWidgetAttributes>.request(attributes: data, contentState: state, pushType: .token)
        orderProgressActivity = activity
        Task {
              for await data in activity.pushTokenUpdates {
                 let myToken = data.map {String(format: "%02x", $0)}.joined()
                  print(myToken);
                 // 将token告知后端用于远程推送
              }
        }

运行CommandLine
配置所需要使用的宏:

$ export TEAM_ID={TEAM ID}
$ export TOKEN_KEY_FILE_NAME={Token Key file path}                       
$ export AUTH_KEY_ID={your Auth Key ID}
$ export DEVICE_TOKEN={myToken from the activity push token}                设备Token(灵动岛是Activity start后返回的pushtoken)
$ export APNS_HOST_NAME=api.sandbox.push.apple.com

说明:
TEAM_ID:开发者TeamID(开发者账户可查)
TOKEN_KEY_FILE_NAME:开发者账户生成的Keys下载后的文件路径(.p8格式)
AUTH_KEY_ID:开发者账户生成的keys的ID(开发者账号内可查、下载的.p8文件默认后缀前的十个字符)
DEVICE_TOKEN:设备Token(灵动岛场景下是Activity指定pushType为.token后start返回的pushtoken)

配置命令行推送(命令行逐行运行,注意中英字符):

$ 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}"

发送推送:

curl -v \
--header "apns-topic:po.test.push-type.liveactivity" \
--header "apns-push-type:liveactivity" \
--header "authorization:bearer $AUTHENTICATION_TOKEN" \
--data \
'{"Simulator Target Bundle": "po.test",
   "aps": {
   "timestamp":1668764100,            
   "event": "update",
   "content-state": {
      "progressMsg": "酒店订单已确认",
   }
}}' \
--http2 \
https://${APNS_HOST_NAME}/3/device/$DEVICE_TOKEN

apns-topic:固定为{BundleId}.push-type.liveactivity
apns-push-type:固定为liveactivity
Simulator Target Bundle:测试模拟器推送,设置为对应应用的{BundleId}
timestamp:刷新时间戳。需设置正确,否则灵动岛的推送不会生效
event:可填入update、end,对应灵动岛的更新与结束。
content-state:对应灵动岛的Attributes

参考资料

本文只是简单介绍了基础功能的实现,更丰富的探索可查阅官方文档:

https://developer.apple.com/documentation/activitykit/displaying-live-data-with-live-activities
https://developer.apple.com/documentation/widgetkit/dynamicisland/
https://developer.apple.com/documentation/activitykit/update-and-end-your-live-activity-with-remote-push-notifications
https://developer.apple.com/design/human-interface-guidelines/components/system-experiences/live-activities
https://developer.apple.com/news/?id=mis6swzt

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容