货拉拉用户 iOS 端灵动岛实践总结

1. 前言

实时活动是iOS 16.1及以上版本中新增的功能,它允许应用在锁屏界面显示实时数据,能够帮助用户实时查看当前订单的进展,而无需解锁手机。用户在货拉拉APP上下单后,可以将手机放置一旁,开始其他工作。当用户想要查询订单状态时,只需从锁定屏幕或灵动岛上轻松操作即可。实时活动的出现不仅省去了用户解锁手机的步骤,更为用户节省了时间和精力。目前货拉拉APP适配"灵动岛"的最新6.7.68版本已正式上线,欢迎大家升级体验。在适配过程中,货拉拉App也踩过很多"坑",在此汇总为实战经验分享给大家。

2. Live Activity&灵动岛的介绍

Live Activity的实现需要使用Apple的ActivityKit框架。通过使用ActivityKit,开发者可以轻松地创建一个Live Activity,这是一个动态的、实时更新的活动,可以在用户的设备上显示各种信息。此外,ActivityKit还提供了推送通知的功能,开发者可以通过服务器向用户的设备发送更新;这样,即使应用程序没有运行,用户也可以接收到最新的信息。

灵动岛是Live Activity的一种展示形式,灵动岛有三种展示形式:Compact紧凑、Minimal最小化,Expanded扩展。开发时必须实现这三种形式,以确保灵动岛在不同的场景下都能正常展示。

同时还需要实现锁屏下的实时活动UI,设备处于锁屏状态下,也能查看实时更新的内容。以上功能的实现,都是使用WidgetKit和SwiftUI完成开发。

2.1 技术难点及策略

实时活动,主要是APP在后台时,主动更新通知栏和灵动岛的数据,为用户展示最新实时订单状态。如何及时刷新实时活动的数据,是一个重点、难点。

更新方式有3种:

  1. 通过APP内订单状态的变化刷新实时活动和灵动岛。此方法开发量小,但是APP退到后台30s后或者进程杀掉,会停止数据的更新。
  2. 让APP配置支持后台运行模式,通过本地现有的订单状态变化逻辑,在后台发起网络请求,获取订单的数据后刷新实时活动。此方法开发量小,但求主App进程必须存在,进程一旦杀掉就无法更新。
  3. 通过接受远程推送通知来更新实时活动。此方法需要后端配合,此方式比较灵活,无需App进程存在,数据更新及时。也是业界常见的方案。

通过对数据刷新的三种方案进行评估后,选择了用户体验最佳的第三种方式。通过后端发生push,端上接受push数据来更新实时活动。

3. Live Activity&灵动岛的实践

3.1 实现方案流程图

实现流程图:

3.2 实现代码

创建Live Activities的准备:

  • Xcode需要14.1以上版本
  • 在主工程的 Info.plist 文件中添加一个键值对,key 为 NSSupportsLiveActivities,value 为 YES
  • 使用ActivityKit在Widget Extension 中创建一个Live Activity

需要实现锁屏状态下UI、灵动岛长按展开的UI、灵动岛单个UI、多个实时活动时的minimalUI

swift 复制代码
import SwiftUI
import WidgetKit

@main
struct TestWidget: Widget {    
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: TestAttributes.self) { context in
            // 锁屏状态下的UI
        } dynamicIsland: { context in
            DynamicIsland {
                //灵动岛展开后的UI
            } compactLeading: {
                // 未被展开左边UI
            } compactTrailing: {
                // 未被展开右边UI
            } minimal: {
               // 多任务时,右边的一个圆圈区域
            }
            .keylineTint(.cyan)
        }
    }
}

灵动岛主要分为StartUpdateEnd三种状态,可由ActivityKit远程推送控制其状态。

开启Live Activity

csharp 复制代码
        let state = TestAttributes.ContentState()
        let attri = TestAttributes(value: 100)
        do {
            let current = try Activity.request(attributes: attri, contentState: state, pushType: .token)
            Task {
                for await state in current.contentStateUpdates {
                    //监听state状态
                }
            }
            Task {
                for await state in current.activityStateUpdates {
                 //监听activity状态
                }
            }
        } catch(let error) {
        }

更新Live Activity

swift 复制代码
   Task {
            guard let current = Activity<TestAttributes>.activities.first else {
                return
            }
            let state = TestAttributes.ContentState(value: 88)
            await current.update(using: state)
        }

结束Live Activity

rust 复制代码
    Task {
            for activity in Activity<TestAttributes>.activities {
              await activity.end(dismissalPolicy: .immediate)
            }
        }

4. 使用ActivityKit推送通知

ActivityKit提供了接收推送令牌的功能,我们可以使用这个令牌来通过ActivityKit推送通知从我们的服务器向Apple Push Notification service (APNs)发送更新。

推送更新Live Activity的准备:

  • 在开发者后台配置生成p8证书,替换原来的p12证书

  • 通过pushTokenUpdates获取推送令牌PushToken

  • 向后端注册PushToken

代码展示:

javascript 复制代码
//取得PushToken
for await tokenData in current.pushTokenUpdates {
    let mytoken = tokenData.map { String(format: "%02x", $0) }.joined()
    //向后端注册
    registerActivityToken(mytoken)
}

4.1 模拟器push验证测试

环境要求:

Xcode >= 14.1 MacOS >= 13.0

准备工作:

  1. 通过pushTokenUpdates获取推送需要的token
  2. 根据开发者TeamID、p8证书本地路径、BuidleID等进行脚本配置

脚本示例:

bash 复制代码
export TEAM_ID=YOUR_TEAM_ID
export TOKEN_KEY_FILE_NAME=YOUR_AUTHKEY_FILE.p8
export AUTH_KEY_ID=YOUR_AUTHKEY_ID
export DEVICE_TOKEN=YOUR_PUSH_TOKEN
export APNS_HOST_NAME=api.sandbox.push.apple.com

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:YOUR_BUNDLE_ID.push-type.liveactivity" \
--header "apns-push-type:liveactivity" \
--header "authorization: bearer $AUTHENTICATION_TOKEN" \
--data \
'{"Simulator Target Bundle": "YOUR_BUNDLE_ID",
"aps": {
  "timestamp":1689648272,
   "dismissal-date":0,
   "event": "update",
    "sound":"default",
   "content-state": {
      "title": "等待付款",
      "content": "请尽快完成下单"
   }
}}' \
--http2 \
https://${APNS_HOST_NAME}/3/device/$DEVICE_TOKEN

其中:

apns-topic:固定为{BundleId}.push-type.liveactivity

apns-push-type:固定为liveactivity

Simulator Target Bundle:模拟器推送,设置为对应APP的BundleId

timestamp:表示推送通知的发送时间,如果timestamp字段的值与当前时间相差太大,可能会收不到推送。

event:可填入update、end,对应Live Activity的更新与结束。

dismissal-date:当event为end时有效,表示结束后从锁屏上移除Live Activity的时间。如果推送内容不包含"dismissal-date",默认结束后4小时后消失,但内容不会再发生更新。如果期望Live Activity结束后立即从锁屏上移除它,可为"dismissal-date"提供一个过去的日期。

content-state:对应灵动岛的Activity.ContentState;如果push中content-state的字段和Attributes比较:

  • 字段过多,多余的字段可能会被忽略,不会导致解析失败

  • 字段缺少,会在解析push通知时出现问题错误。错误表现为:实时活动会有蒙层,并展示loading菊花UI。

示范:

5. 踩坑记录

  • 在模拟器上无法获取到pushToken,无法进行推送模拟?

    检查电脑的系统版本号,需要13.0以上

  • 更新实时活动时,页面显示加载loadingUI,为什么?

    核对push字段和Activity.ContentState的字段是否完全一致,字段少了会解析失败

  • 在16.1系统上,无法展示实时活动,其他更高系统能展示?

    检查Widget里面iOS系统版本号的配置,设置为想要支持的最低版本

  • dismissal-date设置为10分钟后才消失,为什么Dynamic Island灵动岛立即消失了?

    Dynamic Island的显示逻辑可能会更加复杂,如果push的event=end,Dynamic Island灵动岛会立即消失。期望同时消失,可以在指定时间再发end,dismissal-date设置为过去时间,锁屏UI和Dynamic Island灵动岛会同时消失。

  • 推送不希望打扰用户,静默推送,不需要震动和主动弹出,如何设置?

    将"content-available"设置为1,"sound" 设置为: ""

json 复制代码
"aps" = {
        "content-available" : 1,
        "sound" : ""
    }
  • 用户系统是深色模式时,如何适配?

    可以使用@Environment(.colorScheme)属性包装器来获取当前设备的颜色模式。会返回一个ColorScheme枚举,它可以是.light.dark。在根据具体的场景进行UI适配

scss 复制代码
struct ContentView: View {
    @Environment(.colorScheme) var colorScheme

    var body: some View {
        VStack {
            if colorScheme == .dark {
                Text("深夜模式")
                    .foregroundColor(.white)
                    .background(Color.black)
            } else {
                Text("日间模式")
                    .foregroundColor(.(.black)
                    .background(Color.white)
            }
        }
    }
}

5.1 场景限制及建议

  1. 官方文档提示实时活动最多持续8小时,8小时后数据无法刷新,12小时后会强制消失。因此8小时后的数据不准确
  2. 实时活动的卡片上禁止定位以及网络请求,数据需要小于4KB,不能展示特别负责庞大的数据
  3. 同场景多卡片由于样式趋同且折叠,不建议同时创建多卡片。用户多次下单时,建议只处理第一个订单

6. 用户APP上线效果

用户端iOS APP灵动岛上线后的部分场景截图:

7. 总结

灵动岛功能自上线以来,经过我们的数据统计,用户实时活动使用率高达75%以上。这一数据的背后,是灵动岛强大的功能和优秀的用户体验。用户可以在锁屏页直接查看订单状态,无需繁琐的操作步骤,大大提升了用户体验。这种便捷性,使得灵动岛在用户中的接受度较高。

我们的方案不仅可以应用于当前的业务场景,后续还计划扩展到营销活动,定制化通知消息等多种业务场景。这种扩展性,使得灵动岛可以更好地满足不同用户的需求,丰富产品运营策略。

我们希望通过分享开发过程中遇到的问题和解决方案,可以帮助到更多的人。如果你有任何问题或者想法,欢迎在评论区留言。期待我们在技术的道路上再次相遇。

总的来说,灵动岛以其高效、便捷、灵活的特性,赢得了用户的广泛好评。我们将继续努力,为用户提供更优质的服务,为产品的发展注入更多的活力。

相关推荐
Jinkey5 小时前
FlutterBasic - GetBuilder、Obx、GetX<Controller>、GetxController 有啥区别
android·flutter·ios
程序猿看视界11 小时前
如何在 UniApp 中实现 iOS 版本更新检测
ios·uniapp·版本更新
dr李四维14 小时前
iOS构建版本以及Hbuilder打iOS的ipa包全流程
前端·笔记·ios·产品运营·产品经理·xcode
️ 邪神15 小时前
【Android、IOS、Flutter、鸿蒙、ReactNative 】自定义View
flutter·ios·鸿蒙·reactnative·anroid
比格丽巴格丽抱1 天前
flutter项目苹果编译运行打包上线
flutter·ios
网络安全-老纪1 天前
iOS应用网络安全之HTTPS
web安全·ios·https
1024小神1 天前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
lzhdim1 天前
iPhone 17 Air看点汇总:薄至6mm 刷新苹果轻薄纪录
ios·iphone
安和昂1 天前
【iOS】知乎日报第四周总结
ios
麦田里的守望者江1 天前
KMP 中的 expect 和 actual 声明
android·ios·kotlin