Много информации ≠ много кода
Документация 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.
Очевидно, здесь можно передать для отрисовки что-то более сложное, чем строки. Что и в каком именно месте будет отображаться, выбирать вам.
Динамический остров может быть полезен, когда вы хотите показать обновленную информацию о текущей задаче. Например, обновления в реальном времени для спортивного или навигационного приложения, информация обратного отсчета приложения таймера, элементы управления музыкой для музыкальных приложений, данные о загрузке или скачивания файла и т.д.
(прим. переводчиков)
Gargo
Пожалуйста, добавьте информацию о том, на каких устройствах эта фишка доступна. Я так понимаю пока только iphone 14 Pro и iphone 14 Pro Max — т.е. даже не все iphone 14?
fugasio
Ну, потому что 14 айфоны остались с той же челкой что и были?
troy_brox Автор
Спасибо за замечание! Да, действительно, данная система впервые появилась на моделях iPhone 14 Pro и iPhone 14 Pro Max, и сейчас есть только на них, добавил данную информацию)