零、关于灵动岛的认识

灵动岛,即实时活动(Live Activity)

它答应人们以瞥见的形式来观察事情或使命的状态.我的了解是”我不需求一向盯着看,可是我偶尔想看的时分能很方便的看到”.这就需求再规划的时分尽或许扔掉没用的信息,保持信息的简练.

实时活动的事情构成最好是包括清晰开端 + 完毕的事情.例如:外卖、球赛等.

实时活动在完毕前最多存活8小时,完毕后在锁屏界面最多再保存4小时.

关于更多灵动岛(实时活动)的最佳实践及规划思路能够参阅一下: 知乎-苹果开放第三方App登岛,灵动岛规划指南来了!

一、灵动岛的UI布局

接入灵动岛后,有的设备支持(iPhone14Pro / iPhone14ProMax)展现灵动岛+告诉中心,有的设备不支持灵动岛则只在告诉中心展现一条实时活动的告诉. 所以以下四种UI都需求实现:

1.紧凑型

iOS 灵动岛上岛指南
2.最小型
iOS 灵动岛上岛指南
3.扩展型
iOS 灵动岛上岛指南
4.告诉
iOS 灵动岛上岛指南

二、代码实现

1.在主工程中创立灵动岛Widget工程

Xcode -> Editor -> Add Target

iOS 灵动岛上岛指南

如图勾选即可

iOS 灵动岛上岛指南

2.在主工程的info.plist中增加key

Supports Live Activities = YES (答应实时活动)

Supports Live Activities Frequent Updates = YES(实时活动支持频频更新) 这个看项目的需求,不是强制的

iOS 灵动岛上岛指南

3.增加主工程与widget数据交互模型

在主工程中,新建Swift File,作为交互模型的文件.这儿将数据管理与模型都放到这一个文件里了.

iOS 灵动岛上岛指南

创立文件后的目录结构

iOS 灵动岛上岛指南

import Foundation
import ActivityKit
//整个数据交互的模型
struct TestWidgetAttributes: ActivityAttributes {
  public typealias TestWidgetState = ContentState
  //可变参数(动态参数)
  public struct ContentState: Codable, Hashable {
    var data: String
  }
  //不可变参数 (整个实时活动都不会改动的参数)
  var id: String
}

如果参数过多.或者与OC混编,默许给出的这种结构体或许无法满意要求.此时能够运用独自的模型目标,这样OC中也可直接结构与赋值.注意,此处的模型需求遵从Codable协议

import Foundation
import ActivityKit
struct TestWidgetAttributes: ActivityAttributes {
  public typealias TestWidgetState = ContentState
  //可变参数(动态参数)
  public struct ContentState: Codable, Hashable {
    var dataModel: TestLADataModel
  }
  //不可变参数 (整个实时活动都不会改动的参数)
  //var name: String
}
@objc public class TestLADataModel: NSObject, Codable {
  @objc var idString : String = ""
  @objc var nameDes : String = ""
  @objc var contentDes : String = ""
  @objc var completedNum : Int//已完结人数
  @objc var notCompletedNum : Int//未完结人数
  var allPeopleNum : Int {
    get {
      return completedNum + notCompletedNum
    }
  }
  public override init() {
    self.nameDes = ""
    self.contentDes = ""
    self.completedNum = 0
    self.notCompletedNum = 0
    super.init()
  }
  /// 便当结构
  @objc convenience init(nameDes: String, contentDes: String, completedNum: Int, notCompletedNum: Int) {
    self.init()
    self.nameDes = nameDes
    self.contentDes = contentDes
    self.completedNum = completedNum
    self.notCompletedNum = notCompletedNum
  }
}

4.Liveactivity widget的UI

打开前文创立的widget,我的叫demoWLiveActivity.swift

这儿给出了默许代码的注释,详细的布局代码就不再此处赘述了.

import ActivityKit
import WidgetKit
import SwiftUI
struct demoWLiveActivity: Widget {
  var body: some WidgetConfiguration {
    ActivityConfiguration(for: demoWAttributes.self) { context in
      // 锁屏之后,显现的桌面告诉栏方位,这儿能够做相对杂乱的布局
      VStack {
        Text("Hello")
      }
      .activityBackgroundTint(Color.cyan)
      .activitySystemActionForegroundColor(Color.black)
    } dynamicIsland: { context in
      DynamicIsland {
        /*
        这儿是长按灵动岛[扩展型]的UI
        有四个区域限制了布局,分别是左、右、中间(硬件下方)、底部区域
        */
        DynamicIslandExpandedRegion(.leading) {
          Text("Leading")
        }
        DynamicIslandExpandedRegion(.trailing) {
          Text("Trailing")
        }
        DynamicIslandExpandedRegion(.center) {
          Text("Center")
        }
        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)
    }
  }
}

5.Liveactivity 的发动 / 更新(主工程) / 中止

发动

let attributes = TestWidgetAttributes()
let initialConetntState = TestWidgetAttributes.TestWidgetState(dataModel: dataModel)
do {
        let activity = try Activity.request(
            attributes: attributes,
            content: .init(state: initialConetntState, staleDate: nil),
            pushType: nil
//       pushType: .token
        )
        print("恳求敞开实时活动: \(activity.id)")
    } catch (let error) {
        print("恳求敞开实时犯错: \(error.localizedDescription)")
    }

更新

let updateState = TestWidgetAttributes.TestWidgetState(dataModel: dataModel)
let alertConfig = AlertConfiguration(
    title: "\(dataModel.nameDes) has taken a critical hit!",
    body: "Open the app and use a potion to heal \(dataModel.nameDes)",
    sound: .default
)
await activity.update(
    ActivityContent<TestWidgetAttributes.ContentState>(
        state: updateState,
        staleDate: nil
    ),
    alertConfiguration: alertConfig
)
print("更新实时活动: \(activity.id)")

完毕

let finalContent = TestWidgetAttributes.ContentState(
    dataModel: TestLADataModel()
)
let dismissalPolicy: ActivityUIDismissalPolicy = .default
await activity.end(
    ActivityContent(state: finalContent, staleDate: nil),
    dismissalPolicy: dismissalPolicy)
removeActivityState(id: idString);
print("完毕实时活动: \(activity)")

三、更新数据

数据的更新主要经过两种办法:

1.服务端推送

2.主工程更新

其中主工程的更新参见(2.5.Liveactivity 的发动 / 更新(主工程) / 中止)

这儿主要讲经过推送办法的更新

首要为主工程敞开推送功能,但不要运用registerNotifications()为ActivityKit推送告诉注册您的实时活动,详细的注册办法见下.

iOS 灵动岛上岛指南

1. APNs 认证办法挑选

APNs认证办法分为两种: 1.cer证书认证

2.Token-Based认证办法

此处只能挑选Token-Based认证办法,挑选cer证书认证发送LiveActivity推送时,会报TopicDisallowed过错.

Token-Based认证办法的key生产办法 参见:Apple Documentation – Establishing a token-based connection to APNs

2. Liveactivity 的发动

let attributes = TestWidgetAttributes()
let initialConetntState = TestWidgetAttributes.TestWidgetState(dataModel: dataModel)
do {
    let activity = try Activity.request(
        attributes: attributes,
        content: .init(state: initialConetntState, staleDate: nil),
        pushType: .token//须运用此值,声明发动需求获取token
    )
    //判别发动成功后,获取推送令牌 ,发送给服务器,用于远程推送Live Activities更新
    //不是每次发动都会成功,当现已存在多个Live activity时会出现发动失利的状况
    print("恳求敞开实时活动: \(activity.id)")
    Task {
        for await pushToken in activity.pushTokenUpdates {
            let pushTokenString = pushToken.reduce("") { $0 + String(format: "%02x", $1) }
            //这儿拿到用于推送的token,将该值传给后端
            pushTokenDidUpdate(pushTokenString, pushToken);
        }
    }
} catch (let error) {
    print("恳求敞开实时犯错: \(error.localizedDescription)")
}

3. 模仿推送

1.能够运用terminal简略的构建推送,这儿随便放上一个栗子

2.也能够运用这个东西 – SmartPushP8

此处运用SmartPushP8

iOS 灵动岛上岛指南

宣布推送后,在设备上查看推送成果即可.

注意:模仿器也是能够收到liveactivity的推送的可是需求满意:运用T2安全芯片 or 运行macOS13以上的 M1 芯片设备

四、问题排查

推送失利:

1.TooManyProviderTokenUpdates

测验环境对推送次数有一定的限制.测验切换线路(sandbox / development)能够获得更多推送次数.

如果切换线路无法解决问题,建议重新run一遍工程,这样能够获得新的deviceToken,完结推送测验.

2.InvalidProviderToken / InternalServerError

测验重新挑选证书,重新run工程吧…暂时无解.

推送成功,但设备并未收到更新

这种状况需求打开控制台来观察日志

1.挑选对应设备

2.点击过错和故障

3.过滤条件中增加这三项进程 liveactivitiesd apsd chronod

4.点击开端

iOS 灵动岛上岛指南

鄙人方能够看到过错日志.

demo参阅:demo

五、参阅链接

AppleDeveloper – ActivityKit

AppleDeveloper – Sending notification requests to APNs

HarryDeng – iOS灵动岛开发实践

Xu Jiwei – iOS 运用推送告诉更新 Dynamic Island 和 Live Activity