Live Activity объединяют пуш-уведомления в один интерактивный баннер. Представим приложение для вызова такси — какие там могут быть пуши? «Водитель едет», «Водитель уже рядом» и «Водитель ждёт». С новым инструментом разработчики смогут объединить пуши в Live Activity и обновлять её.
Live Activity не виджет — у неё нет таймлайнов и обновлений по времени. Основной способ обновления — как раз пуши. Способы обновления разберём в секции Как обновить и завершить Live Activity.
Live Activity показываются на устройствах с Dynamic Island и без него. На заблокированном экране это будет похоже на обычное пуш-уведомление. А для устройств с Dynamic Island Live Activity показывается вокруг камер.
Добавляем Live Activity в проект
Live Activity используют фреймворк ActivityKit. Живут Live Activity в таргете виджета:
Перейдите в таргет и оставьте код:
@main
struct LiveActivityWidget: Widget {
let kind: String = "LiveActivityWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
widgetEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}
В Info.plist добавьте атрибут Supports Live Activities:
<key>NSSupportsLiveActivities</key>
<true/>
StaticConfiguration используется для виджетов и компликейшнов. Скоро мы заменим его на другой, но сначала определим модель данных.
Определяем модель данных
Live Activity создаётся в самом приложении, а модель будет использоваться и в приложении, и в виджете. Поэтому хорошо бы сделать один класс и пошарить его между таргетами. Создайте новый файл для модели. Для этого наследуемся от ActivityAttributes:
import ActivityKit
struct ActivityAttribute: ActivityAttributes {
public struct ContentState: Codable, Hashable {
// Динамические данные
var dynamicStringValue: String
var dynamicIntValue: Int
var dynamicBoolValue: Bool
}
// Статические данные
var staticStringValue: String
var staticIntValue: Int
var staticBoolValue: Bool
}
В структуре ContentState определяем динамические данные — они будут меняться и обновлять UI. За пределами ContentState статические данные, они доступны только при создании Live Activity.
Пошарьте файл между двумя таргетами, для этого в инспекторе справа выберите главный таргет приложения и таргет виджета:
UI
В объекте LiveActivityWidget поменяйте конфигурацию на ActivityConfiguration:
struct LiveActivityWidget: Widget {
let kind: String = "LiveActivityWidget"
var body: some WidgetConfiguration {
ActivityConfiguration(for: ActivityAttribute.self) { context in
// Здесь UI для активити на заблокированном экране
} dynamicIsland: { context in
// Здесь UI для Dynamic Island
}
}
}
У нас есть два замыкания, первое — для UI на заблокированном экране, второе — для динамического острова. Обратите внимание, указываем класс атрибутов ActivityAttribute.self — это модель данных, которую определили выше.
Lock Screen
Эта View показывается на заблокированном экране. Все инструменты для виджетов доступны в Live Activity. Укажите проперти context, чтобы передать модель данных:
struct LockScreenLiveActivityView: View {
let context: ActivityViewContext<ActivityAttribute>
var body: some View {
VStack {
Text("Dyanmic String: \(context.state.dynamicStringValue))")
Text("Static String: \(context.staticStringValue))")
}
.activitySystemActionForegroundColor(.indigo)
.activityBackgroundTint(.cyan)
}
}
В примере я распечатал и динамические, и статические проперти из ActivityAttribute. Давайте укажем вью в виджете:
struct LiveActivityWidget: Widget {
let kind: String = "LiveActivityWidget"
var body: some WidgetConfiguration {
ActivityConfiguration(for: ActivityAttribute.self) { context in
LockScreenLiveActivityView(context: context)
} dynamicIsland: { context in
// Здесь UI для Dynamic Island
}
}
}
Dynamic Island
У динамического острова есть 3 вида: компактный, минимальный и развёрнутый.
Compact & Minimal
Если запущена одна активность, то контент можно разместить слева и справа от динамического острова.
Если запущено несколько Live Activity, система выберет две из них. Одна будет показываться слева, она прикреплена к острову, а другая справа — отделённая от острова в кружке.
Так выглядит код для каждого варианта отображения:
DynamicIsland {
// Здесь будет код для развёрнутого вида.
// Его разберем в след. пункте.
} compactLeading: {
Text("Leading")
} compactTrailing: {
Text("Trailing")
} minimal: {
Text("Min")
}
Expanded
Развёрнутая Live Activity показывается, когда человек нажимает и удерживает компатный или минимальный вид. Когда Live Activity обновляется, развёрнутый вид появляется автоматически на пару секунд.
А вот код для развёрнутого вида. Каждое замыкание определяет область на Live Activity.
DynamicIslandExpandedRegion(.center) {}
DynamicIslandExpandedRegion(.leading) {}
DynamicIslandExpandedRegion(.trailing) {}
DynamicIslandExpandedRegion(.bottom) {}
Разметка областей:
- center — контент под камерой.
- leading — пространство от левого угла до камеры. Если использовать вертикальный стек, будет доступно пространство ниже.
- trailing — аналогично leading, но для правого края.
- bottom — контент под всеми другими областями.
Если контент в левой и правой областях не помещается, можно объединить его с Bottom. Область будет адаптивная, на скриншоте сейчас максимальные размеры:
Чтобы разрешить области использовать пространство ниже, укажите verticalPlacement:
DynamicIslandExpandedRegion(.leading) {
Text("Leading Text with merge region")
.dynamicIsland(verticalPlacement: .belowIfTooWide)
}
Как добавить новую Live Activity
Live Activity можно создать только внутри приложения. Обновить и закончить Live Activity можно и внутри приложения, и по пуш-уведомлению.
Сначала проверьте доступность Live Activity — пользователь мог запретить их. Вторая причина недоступности — в системе достигнут лимит. Чтобы проверить, используем код:
guard ActivityAuthorizationInfo().areActivitiesEnabled else {
print("Activities are not enabled")
return
}
Можно отслеживать статус:
for await enabled in ActivityAuthorizationInfo().activityEnablementUpdates {
// Здесь ваш код
}
Чтобы создать новую Live Activity, создайте атрибуты и после вызовите request:
let attributes = ActivityAttribute(...)
let contentState = ActivityAttribute.ContentState(...)
do {
let activity = try Activity<ActivityAttribute>.request(
attributes: attributes,
contentState: contentState
)
} catch {
print("LiveActivityManager: Error in LiveActivityManager: \(error.localizedDescription)")
}
Обратите внимание - здесь разделились статические и динамические проперти на два объекта.
Список активных Live Activity
Чтобы получить уже созданные Live Activity, укажите модель атрибутов:
for activity in Activity<ActivityAttribute>.activities {
print("Activity details: \(activity.contentState)")
}
Обновить и завершить Live Activity
Обновлять и завершать Live Activity можно только с динамическими параметрами — Content State.
Внутри приложения
Как обновить Live Activity из приложения:
// Новые данные
let contentState = ActivityAttribute.ContentState(...)
Task {
await activity?.update(using: contentState)
}
Чтобы завершить Live Activity, вызовите:
await activity?.end(dismissalPolicy: .immediate)
Live Activity закроется сразу. А вот как сделать, чтобы Live Activity осталась ещё некоторое время на экране:
await activity?.end(using: attributes, dismissalPolicy: .default)
Live Activity обновится финальными данными и будет на экране ещё некоторое время. Система закроет активность через 4 часа или когда убедится, что пользователь увидел новые данные. Зависит от того, что наступит раньше.
У Live Activity нет таймлайна, как для виджетов. Для обновления или закрытия Live Activity — когда приложение в фоне — используйте Background Tasks.
Через push-уведомления
При создании Live Activity получаем pushToken. Он используется, чтобы обновлять Live Activity через пуш-уведомления.
Сформируем пуш для обновления Live Activity. Заголовки:
apns-topic: {Your App Bundle ID}.push-type.liveactivity
apns-push-type: liveactivity
authorization: bearer {Auth Token}
Тело:
"aps": {
"timestamp": 1168364460,
"event": "update", // or end
"content-state": {
"dynamicStringValue": "New String Value"
"dynamicIntValue": 5
"dynamicBoolValue": true
},
"alert": {
"title": "Title of classic Push",
"body": "Body or classic push",
}
}
Словарь content-state должен совпадать с моделью атрибутов ActivityAttribute.ContentState. Мы можем обновлять только динамические проперти. Проперти вне Content State обновить не получится.
Отследить нажатие на Live Activity
По нажатию на Live Activity хорошо открывать релевантный экран, для этого реализуйте Deep Link. Установите модификатор widgetURL(_:). Можно задать разные ссылки для каждой области:
DynamicIslandExpandedRegion(.leading) {
Text("Leading Text with merge region")
.widgetURL(URL(string: "example://action"))
}
Развёрнутый вид Dynamic Island поддерживает Link.