Много информации ≠ много кода

Документация Apple рассказывает, как начать работу с Dynamic Island, динамическим островом. Система была представлена Apple в сентябре 2022 года, на данный момент она есть только в моделях iPhone 14 Pro и iPhone 14 Pro Max. С Dynamic Island можно анимированно показывать информацию вокруг области выреза фронтальной камеры iPhone, которую мы привыкли называть «чёлкой».
В этой статье мы рассмотрим пример базовой работы с размещением контента в Dynamic Island для его разных состояний.
Для сборки проекта нужно запустить Xcode версии не ниже 14.1 Beta.
Этот пример основан на документации Apple. Ещё вы увидите работу с данными, которые отправляются в Activity в Dynamic Island.
Activity — это практически виджет, как виджеты в iOS 14. Мы настраиваем виджет для разных состояний и объявляем его пользовательский интерфейс с помощью SwiftUI. Основное приложение добавляет Activity, потом удаляет его и обновляет информацию, отправляя полезные данные.
Ещё один способ обновить Live Activity — использовать push-уведомления. В отличие от других виджетов, Live Activity не может обновляться, выходя в сеть, поэтому это делает основное приложение или push-уведомления.
(прим. переводчиков)
В конце реализации мы получим следующий результат:

У нас есть два view для компактного состояния (compact) и четыре view для расширенного (expanded).
Compact (компактный) — «обычное» состояние, когда мы выходим из приложения, и оно «сжимается» в динамический остров.
Expanded (расширенный) — когда пользователь удерживает нажатие на динамическом острове, Activity временно расширяется, чтобы получить больше места и элементов управления.
(прим. переводчиков)
Создайте новый проект iOS и выберите проект в Project Navigator на панели слева.
Перейдите на вкладку Info в настройках проекта, наведите курсор на последнюю запись, нажмите “+” и добавьте новое свойство. Оно должно называться NSSupportsLiveActivities, значение должно быть типа Boolean, параметр — YES.
Важно, чтобы это было в info.plist таргета приложения, а не в каком-либо из его расширений.
Начнём
Я собираюсь создать Form Section, с помощью которой в дальнейшем будет происходить управление временем жизненного цикла Live Activity.
import SwiftUI struct TimeSliders: View { let title: String @Binding var minutes: Double @Binding var seconds: Double var body: some View { Section(title) { LabeledContent("Minutes", value: minutes, format: .number) Slider(value: $minutes, in: 0...60) { Text("Minutes") } LabeledContent("Seconds", value: seconds, format: .number) Slider(value: $seconds, in: 0...59) { Text("Seconds") } } } }
Для визуализации view, отображаемых при расширенном состоянии Live Activity, система делит область для контента на секции Center, Leading, Trailing и Bottom, как на схеме:

Для сжатого состояния предусмотрены секции CompactLeading и CompactTrailing.
Во view, приведенном ниже, можно увидеть доступные для редактирования пользователем области Dynamic Island. Каждой части дана пользовательская строка, хотя рекомендую, чтобы эти строки были короткими: многие из них предназначены для маленьких иконок. Ещё здесь можно размещать эмодзи ?.
import SwiftUI struct ActivityTextFields: View { @Binding var centreText: String @Binding var bottomText: String @Binding var leadingText: String @Binding var trailingText: String @Binding var compactLeadingText: String @Binding var compactTrailingText: String var body: some View { Section("Centre Text") { TextField("Centre Text", text: $centreText) } Section("Bottom Text") { TextField("Bottom Text", text: $bottomText) } Section("Leading Text") { TextField("Leading Text", text: $leadingText) } Section("Trailing Text") { TextField("Trailing Text", text: $trailingText) } Section("Compact Leading Text") { TextField("Compact Leading Text", text: $compactLeadingText) } Section("Compact Trailing Text") { TextField("Compact Trailing Text", text: $compactTrailingText) } } }
Ниже приведена моя реализация протокола ActivityAttributes. С его помощью хранится текст, отображающийся в определенных позициях Dynamic Island.
import Foundation import ActivityKit struct MyActivityAttributes: ActivityAttributes { public struct ContentState: Codable, Hashable { var timerRange: ClosedRange<Date> } var bottomText: String var centreText: String var leadingText: String var trailingText: String var compactLeadingText: String var compactTrailingText: String var minimalText: String }
Эту структуру я буду использовать для отображения текущей Live Activity в реальном времени после создания.
Обратите внимание, что я использую Section с названием Time Left, который передает timerInterval в Text. Это новый простой способ отображения таймера с использованием только Text и ClosedRange<Date>. Создание этого объекта ClosedRange<Date> произойдет позже, но его можно использовать, если нужен таймер в Dynamic Island или в виджете на экране блокировки.
import SwiftUI import ActivityKit struct ActivityDetailsView: View { let activity: Activity<MyActivityAttributes>? let timerRange: ClosedRange<Date> var body: some View { if let activity { Section("Time Left") { Text(timerInterval: timerRange) } Section ("Text") { LabeledContent("Arrival Time", value: timerRange.upperBound, format: .dateTime) LabeledContent("Centre Text", value: activity.attributes.centreText) LabeledContent("Bottom Text", value: activity.attributes.bottomText) LabeledContent("Leading Text", value: activity.attributes.leadingText) LabeledContent("Trailing Text", value: activity.attributes.trailingText) LabeledContent("Compact Leading Text", value: activity.attributes.compactLeadingText) LabeledContent("Compact Trailing Text", value: activity.attributes.compactTrailingText) } } } }

Я сохраняю все данные в моём типе ContentView, но пока он не соответствует протоколу View. Функция createActivity использует новый тип MyActivityAttributes для создания Activity с выбранными пользователем данными.
import ActivityKit import SwiftUI struct ContentView { @State var activity: Activity<MyActivityAttributes>? @State var minutes = 1.0 @State var seconds = 0.0 @State var future = Date.distantFuture @State var timerRange = Date.now...Date.distantFuture @State var bottomText = "B" @State var centreText = "C" @State var leadingText = "L" @State var trailingText = "T" @State var compactLeadingText = "A" @State var compactTrailingText = "B" @State var minimalText = "M" @State var activityExpanded = true func createActivity() { future = Calendar.current .date(byAdding: .minute, value: Int(minutes), to: Date())! future = Calendar.current .date(byAdding: .second, value: Int(minutes), to: future)! timerRange = Date.now...future let initialContentState = MyActivityAttributes .ContentState(timerRange: timerRange) let activityAttributes = MyActivityAttributes( bottomText: bottomText, centreText: centreText, leadingText: leadingText, trailingText: trailingText, compactLeadingText: compactLeadingText, compactTrailingText: compactTrailingText, minimalText: minimalText ) do { activity = try Activity .request(attributes: activityAttributes, contentState: initialContentState) print("Requested Live Activity \(String(describing: activity?.id)).") activityExpanded.toggle() } catch (let error) { print("Error requesting Live Activity \(error.localizedDescription).") } } }
Теперь я добавлю соответствие протоколу View.
Функция createActivity теперь будет добавлена как действие при нажатии Button в Form. Она будет сворачивать DisclosureGroup с параметрами Activity, чтобы отобразить данные Activity в ActivityDetailsView. TimeSliders и ActivityTextFields будут скрыты, но DisclosureGroup можно открыть снова, чтобы при необходимости создать другую Activity.
import SwiftUI extension ContentView: View { var body: some View { Form { DisclosureGroup("Activity", isExpanded: $activityExpanded) { TimeSliders(title: "Activity End", minutes: $minutes, seconds: $seconds) ActivityTextFields( centreText: $centreText, bottomText: $bottomText, leadingText: $leadingText, trailingText: $trailingText, compactLeadingText: $compactLeadingText, compactTrailingText: $compactTrailingText ) Button("Start", action: createActivity) } ActivityDetailsView(activity: activity, timerRange: timerRange) } } }
Создание виджета
Переходим к следующему шагу.
Перейдите в File > New > Target… и создайте расширение виджета. Я своё назвал DynamicIslandWidget.
Этот пример довольно прост для понимания, поэтому я не углублялся, чтобы сделать подходящий виджет экрана блокировки. Text("N/A") отобразится в нижней части экрана блокировки, но вы можете менять его на всё, что хотите.
import WidgetKit import SwiftUI @main @available(iOS 16.1, *) struct DynamicIslandWidget: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: MyActivityAttributes.self) { context in Text("N/A") } dynamicIsland: { context in DynamicIsland { DynamicIslandExpandedRegion(.leading) { Text(context.attributes.leadingText) .foregroundColor(.indigo) .font(.title2) } DynamicIslandExpandedRegion(.trailing) { Text(context.attributes.trailingText) } DynamicIslandExpandedRegion(.center) { Text(context.attributes.centreText) .lineLimit(1) .font(.caption) } DynamicIslandExpandedRegion(.bottom) { Text(context.attributes.bottomText) .foregroundColor(.indigo) } } compactLeading: { Text(context.attributes.compactLeadingText) } compactTrailing: { Text(context.attributes.compactTrailingText) } minimal: { Text(context.attributes.minimalText) } .keylineTint(.yellow) } } }
Ниже — результат для сжатого и расширенного состояний Dynamic Island.

Обратите внимание: чтобы приложение свернулось в Dynamic Island, необходимо вернуться к Home Screen.
При одном нажатии на Dynamic Island приложение снова откроется. При удержании откроется расширенное состояние Dynamic Island.
Очевидно, здесь можно передать для отрисовки что-то более сложное, чем строки. Что и в каком именно месте будет отображаться, выбирать вам.
Динамический остров может быть полезен, когда вы хотите показать обновленную информацию о текущей задаче. Например, обновления в реальном времени для спортивного или навигационного приложения, информация обратного отсчета приложения таймера, элементы управления музыкой для музыкальных приложений, данные о загрузке или скачивания файла и т.д.
(прим. переводчиков)
Другие наши статьи по iOS-разработке:
Structured concurrency в Swift: разбираемся с концепций async/await
