背景
2022.9.8苹果发布会上,最引人注目的一个功能灵动岛问世,当然整个发布会也只有这一个功能能拿出来提一嘴。对于用户而言灵动岛是一种新的交互式,刘海屏改成了药片屏。对于开发者而言,我们需要研究一下能为我们的APP做些什么。
灵动岛是什么
灵动岛是iphone14Pro的专属特性,是iphone14pro和4 pro max两个产品的交互式。
在这两个系列中,把刘海屏改为药片屏幕,给了传统的打孔屏幕一个新的观念。
提到灵动岛,就必须知道实时活动Live Activity,实时活动分为两部分,一部分是锁屏界面的展示,一部分是灵动岛展示。如果开发灵动岛就需要同时兼顾锁屏的展示。在锁定屏幕上,Live Activity显示在屏幕底部。(因为灵动岛属于LiveActivity的一部分,后边我们统一使用Live Activity)
灵动岛与Widget的关系
开发Live Activity的代码都在Widget的Target里,使用SwiftUI和WidgetKit来创建Live Activity的用户界面。所以Live Activity的开发跟开发Widget差不多。
与Widget不同的是,Live Activity使用不同的机制来接受更新。Live Activity不使用时间线机制,使用ActivityKit或者通过远程推送通知更新。
Live Activity的要求和限制
除非应用程序或者用户结束,否则Live Activity最多可以保活8小时,超过时间,系统会自动结束。系统会将其从灵动岛中移除。但是Live Activity会一直保留在锁屏上,知道用户删除或者系统4小时后删除,所以Live Activity在锁屏上最多能存活12小时。
每个Live Activity在自己应用的沙盒中运行,无法访问网络和位置更新,要更新数据,需要使用ActivityKit或者使用远程推送。动态数据的大小不能超过4Kb
Live Activity针对锁屏和灵动岛提供不同的视图。锁屏会出现所有设备上。支持灵动岛的设备具有四种视图:紧凑前视图、紧凑尾视图、最小视图和扩展视图。
为确保系统可以在每个位置显示您的 Live Activity,开发时候必须支持所有视图。
一个应用可以启动多个 Live Activity,而一个设备可以从多个应用运行 Live Activity,启动可能会失败,LiveActivity有上限个数
Live Activity的开发
Live Activity的界面开发是Widget的一部分,如果应用已经支持了Widget,就可以将代码写到Widget里。
创建Widget Target
打开Info.plist文件,添加 Supports Live Activity ,设置为YES
创建ActivityAttributes 结构,描述Live Activity的静态和动态数据。
创建ActivityConfiguration通过ActivityAttributes,启动Live Activity
添加代码以配置、启动、更新和结束。
创建Live Activity
import SwiftUI
import WidgetKit
@main
struct PizzaDeliveryActivityWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: PizzaDeliveryAttributes.self) { context in
// Create the view that appears on the Lock Screen and as a
// banner on the Home Screen of devices that don't support the
// Dynamic Island.
// ...
} dynamicIsland: { context in
// Create the views that appear in the Dynamic Island.
// ...
}
}
}
LiveActivity是一个Widget,所以代码是写在Widget的target里边。可以看到灵动岛的主函数就是继承与Wdiget。
上边函数的ActivityConfiguration第一个block就是锁屏界面的view。第二个block为灵动岛view。
创建锁屏界面的视图
系统要求高度不能超过160,超过的话会截掉
@main
struct PizzaDeliveryWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: PizzaDeliveryAttributes.self) { context in
// Create the view that appears on the Lock Screen and as a
// banner on the Home Screen of devices that don't support the
// Dynamic Island.
LockScreenLiveActivityView(context: context)
} dynamicIsland: { context in
// Create the views that appear in the Dynamic Island.
// ...
}
}
}
struct LockScreenLiveActivityView: View {
let context: ActivityViewContext<PizzaDeliveryAttributes>
var body: some View {
VStack {
Spacer()
Text("\(context.state.driverName) is on their way with your pizza!")
Spacer()
HStack {
Spacer()
Label {
Text("\(context.attributes.numberOfPizzas) Pizzas")
} icon: {
Image(systemName: "bag")
.foregroundColor(.indigo)
}
.font(.title2)
Spacer()
Label {
Text(timerInterval: context.state.deliveryTimer, countsDown: true)
.multilineTextAlignment(.center)
.frame(width: 50)
.monospacedDigit()
} icon: {
Image(systemName: "timer")
.foregroundColor(.indigo)
}
.font(.title2)
Spacer()
}
Spacer()
}
.activitySystemActionForegroundColor(.indigo)
.activityBackgroundTint(.cyan)
}
}
创建灵动岛视图
灵动岛视图分为两大部分
一部分是灵动岛默认状态,就是紧凑状态
一部分是长按灵动岛的扩展状态
创建紧凑和最小的视图
默认情况下,灵动岛中的紧凑和最小视图使用黑色背景颜色和白色文本。可以使用修改keylineTint(_:)
import SwiftUI
import WidgetKit
@main
struct PizzaDeliveryWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: PizzaDeliveryAttributes.self) { context in
// Create the view that appears on the Lock Screen and as a
// banner on the Home Screen of devices that don't support the
// Dynamic Island.
// ...
} dynamicIsland: { context in
// Create the views that appear in the Dynamic Island.
DynamicIsland {
// Create the expanded view.
// ...
} compactLeading: {
Label {
Text("\(context.attributes.numberOfPizzas) Pizzas")
} icon: {
Image(systemName: "bag")
.foregroundColor(.indigo)
}
.font(.caption2)
} compactTrailing: {
Text(timerInterval: context.state.deliveryTimer, countsDown: true)
.multilineTextAlignment(.center)
.frame(width: 40)
.font(.caption2)
} minimal: {
VStack(alignment: .center) {
Image(systemName: "timer")
Text(timerInterval: context.state.deliveryTimer, countsDown: true)
.multilineTextAlignment(.center)
.monospacedDigit()
.font(.caption2)
}
}
.keylineTint(.cyan)
}
}
}
创建扩展视图
视图最高160,多余截断
扩展视图分为4部分
center将内容放置在原深感摄像头下方。
leading将内容沿展开的 Live Activity 的前沿放置在原深感摄像头旁边,并在其下方包裹其他内容。
trailing将内容放置在 TrueDepth 摄像头旁边展开的 Live Activity 的后沿,并在其下方包裹其他内容。
bottom将内容置于前导、尾随和居中内容之下。
struct PizzaDeliveryWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: PizzaDeliveryAttributes.self) { context in
// Create the view that appears on the Lock Screen and as a
// banner on the Home Screen of devices that don't support the
// Dynamic Island.
LockScreenLiveActivityView(context: context)
} dynamicIsland: { context in
// Create the views that appear in the Dynamic Island.
DynamicIsland {
// Create the expanded view.
DynamicIslandExpandedRegion(.leading) {
Label("\(context.attributes.numberOfPizzas) Pizzas", systemImage: "bag")
.foregroundColor(.indigo)
.font(.title2)
}
DynamicIslandExpandedRegion(.trailing) {
Label {
Text(timerInterval: context.state.deliveryTimer, countsDown: true)
.multilineTextAlignment(.trailing)
.frame(width: 50)
.monospacedDigit()
} icon: {
Image(systemName: "timer")
.foregroundColor(.indigo)
}
.font(.title2)
}
DynamicIslandExpandedRegion(.center) {
Text("\(context.state.driverName) is on their way!")
.lineLimit(1)
.font(.caption)
}
DynamicIslandExpandedRegion(.bottom) {
Button {
// Deep link into your app.
} label: {
Label("Call driver", systemImage: "phone")
}
.foregroundColor(.indigo)
}
} compactLeading: {
// Create the compact leading view.
// ...
} compactTrailing: {
// Create the compact trailing view.
// ...
} minimal: {
// Create the minimal view.
// ...
}
.keylineTint(.yellow)
}
}
}
使用自定义颜色
系统默认设置的颜色最适合用户。如果要设置自定义颜色,可以使用activityBackgroundTint(_:)activitySystemActionForegroundColor(_:)
设置半透明,使用opacity(_:)
点击处理
跟Widget相同。
锁屏、紧凑视图使用widgetURL(_:)跳转,全局相应。
扩展视图,使用Link,可以单独按钮相应。
启动Live Activity
var future = Calendar.current.date(byAdding: .minute, value: (Int(minutes) ?? 0), to: Date())!
future = Calendar.current.date(byAdding: .second, value: (Int(seconds) ?? 0), to: future)!
let date = Date.now...future
let initialContentState = PizzaDeliveryAttributes.ContentState(driverName: "Bill James", deliveryTimer:date)
let activityAttributes = PizzaDeliveryAttributes(numberOfPizzas: 3, totalAmount: "$42.00", orderNumber: "12345")
do {
deliveryActivity = try Activity.request(attributes: activityAttributes, contentState: initialContentState)
print("Requested a pizza delivery Live Activity \(String(describing: deliveryActivity?.id)).")
} catch (let error) {
print("Error requesting pizza delivery Live Activity \(error.localizedDescription).")
}
只能通过应用程序在前台时候启动,可以在后台运行时更新或者结束,比如使用Background Tasks。
使用远程推送更新和关闭LiveActivity Updating and ending your Live Activity with remote push notifications.
更新Live Activity
应用程序处于前台或后台时,使用此API更新实时活动。还可以使用该功能在 iPhone 和 Apple Watch 上显示警报,告知人们新的 Live Activity 内容
更新数据的大小不能超过 4KB。
var future = Calendar.current.date(byAdding: .minute, value: (Int(minutes) ?? 0), to: Date())!
future = Calendar.current.date(byAdding: .second, value: (Int(seconds) ?? 0), to: future)!
let date = Date.now...future
let updatedDeliveryStatus = PizzaDeliveryAttributes.PizzaDeliveryStatus(driverName: "Anne Johnson", deliveryTimer: date)
let alertConfiguration = AlertConfiguration(title: "Delivery Update", body: "Your pizza order will arrive in 25 minutes.", sound: .default)
await deliveryActivity?.update(using: updatedDeliveryStatus, alertConfiguration: alertConfiguration)
在 Apple Watch 上,系统将title和body属性用于警报。在 iPhone 上,系统不会显示常规警报,而是显示动态岛中展开的实时活动。在不支持动态岛的设备上,系统会在主屏幕上显示一个横幅,该横幅使用您的实时活动的扩展视图。
结束Live Activity
已结束的实时活动将保留在锁定屏幕上,直到用户将其删除或系统自动将其删除。自动删除的控制取决于函数的dismissalPolicy参数。
另外,及时结束了也要包含更新,保证在Live Activity结束后显示最新和最终的结果。
let finalDeliveryStatus = PizzaDeliveryAttributes.PizzaDeliveryStatus(driverName: "Anne Johnson", deliveryTimer: Date.now...Date())
Task {
await deliveryActivity?.end(using:finalDeliveryStatus, dismissalPolicy: .default)
}
实例显示的是default策略,结束后会保留在锁定屏幕上一段时间。用户可以选择移除,或者4个小时候系统自动移除。
要立即从锁屏上移除的话,需要使用immedia属性,或者after指定时间。时间需要指定在4小时之内,超过4小时,系统会主动删除。
移除live activity,只会影响活动的展示,并不会对业务上产生影响。
通过推送来更新和结束Live Activity
{
"aps": {
"timestamp": 1168364460,
"event": "update",
"content-state": {
"driverName": "Anne Johnson",
"estimatedDeliveryTime": 1659416400
},
"alert": {
"title": "Delivery Update",
"body": "Your pizza order will arrive soon.",
"sound": "example.aiff"
}
}
}
跟踪更新
您启动 Live Activity 时,ActivityKit 会返回一个Activity对象。除了id唯一标识每个活动之外,Activity还提供观察内容状态、活动状态和推送令牌更新的序列。使用相应的序列在您的应用中接收更新,使您的应用和 Live Activity 保持同步,并响应更改的数据:
要观察正在进行的 Live Activity 的状态——例如,确定它是处于活动状态还是已经结束——使用.activityStateUpdates
要观察 Live Activity 动态内容的变化,请使用.contentState
要观察 Live Activity 的推送令牌的变化,请使用.pushTokenUpdates
获取活动列表
您的应用可以启动多个 Live Activity。例如,体育应用程序可能允许用户为他们感兴趣的每个现场体育比赛启动现场活动。如果启动多个现场活动,请使用该功能获取有关您的应用程序正在进行的现场活动的通知。跟踪正在进行的 Live Activity 以确保您的应用程序的数据与 ActivityKit 跟踪的活动 Live Activity 同步。activityUpdates
以下代码段显示了披萨外卖应用程序如何检索正在进行的活动列表:
// Fetch all ongoing pizza delivery Live Activities.
for await activity in Activity<PizzaDeliveryAttributes>.activityUpdates {
print("Pizza delivery details: \(activity.attributes)")
}
获取所有活动的另一个用例是维护正在进行的实时活动,并确保您不会让任何活动持续运行超过需要的时间。例如,系统可能会停止您的应用程序,或者您的应用程序可能会在 Live Activity 处于活动状态时崩溃。当应用下次启动时,检查是否有任何活动仍处于活动状态,更新应用存储的 Live Activity 数据,并结束任何不再相关的 Live Activity。
参考文档: