В то время как Android-устройства в целом ушли в направлении простых вырезов в экране под фронтальную камеру или даже подэкранных фронталок, Apple создала совершенно новый пользовательский опыт благодаря своему новому пространству для размещения камеры — «челке» (the notch). Сегодня мы с вами обсудим, как реализовать нечто подобное в iOS.
Виджеты, которые Apple представила в iOS 14, позволяют нам просматривать информацию прямо на наших главных экранах.
Но что, если мы пойдем еще дальше и представим контекстно-зависимую информацию, которая всплывает при необходимости и не задерживается на экране слишком долго? А что, если бы это было реализовано таким образом, чтобы все это гармонично работало с самым большим обновлением для фронтальной панели, которое наши iPhone видели с момента появления челки? Больше никаких «а что, если» — встречайте Dynamic Island.
Мы уже делились своими впечатлениями о Dynamic Island и Live Activities с точки зрения дизайна, и теперь пришло время показать вам, как можно их реализовать.
iPhone 14 Pro и 14 Pro Max — единственные устройства, поддерживающие Dynamic Island, но Live Activity поддерживаются всеми телефонами, на которых может быть установлена iOS 16.1. Итак, давайте разберемся, что необходимо для их интеграции в ваши приложения.
Жизненный цикл Live Activity
Жизненный цикл — это одна из самых важных вещей, которые следует учитывать при создании приложений. Live Activity может находиться в трех состояниях:
Еще не запущено
Запущено
Завершено
Ваше приложение может иметь несколько Live Activity одновременно, поэтому очень важно отслеживать каждое Activity. Перед запуском Activity необходимо указать, какие данные оно должно использовать. Поскольку мы говорим о Live Activity, вы можете догадаться, что некоторые данные будут меняться с течением времени. Данные вашей Live Activity разделены на динамические и статические. Динамические данные могут обновляться с течением времени, а статические — нет.
В качестве примера давайте рассмотрим FormulaAttributes
, которая будет содержать оба типа данных:
struct FormulaAttributes: ActivityAttributes {
public typealias RaceState = ContentState
// Здесь вы предоставляете динамические данные
public struct ContentState: Codable, Hashable {
var driverInFront: String
var driverTeam: String
}
// А здесь вы предоставляете статические данные
var lastPlaceDriver: String
}
После того, как Activity было запущено, оно может существовать в течение восьми часов, прежде чем оно будет остановлена системой. Восьми часов более чем достаточно для большинства вариантов использования Activity, но иногда у пользователей могут случаться восьмичасовые перелеты, и тогда это восьмичасовое ограничение может вызывать некоторые сложности.
Как только Live Activity запущено и работает, вы можете обновлять динамические данные этого Activity. Следует отметить, что вы не можете обновлять Live Activity посредством вызовов API, как вы можете это делать с другими виджетами. Вместо этого вы должны обновлять его через ваше приложение или push-уведомления.
После того, как вы завершите его, по умолчанию Live Activity по-прежнему будет доступно на экране блокировки еще четыре часа. Вы можете указать, хотите ли вы немедленно уничтожить Live Activity на экране блокировки, в противном случае система сама сделает это за вас через четыре часа.
Убедитесь, что вы обновили Live Activity после его завершения, если вы не сразу удаляете его с экрана блокировки, чтобы отобразить правильное состояние. Таким образом, ваши пользователи будут видеть актуальную информацию и знать, что Live Activity было завершено с последними результатами.
Сколько у Live Activity представлений?
Как упоминалось ранее, вы можете отображать Live Activity на экране блокировки, Dynamic Island или баннере на устройствах, которые не поддерживают Dynamic Island. При разработке для устройств с поддержкой Dynamic Island существует пять представлений:
Минимальное (Minimal)
Ведущее (Leading)
Замыкающее (Trailing)
Расширенное (Expanded)
Для экрана блокировки (Lock Screen)
Каждое из этих представлений может сильно отличаться от остальных.
Минимальное представление
Как следует из названия, оно должно быть минимальным. Например, это может быть изображение, которое однозначно представляет ваше приложение. Минимальное представление отображается с правой стороны от Dynamic Island (с небольшим отступом), когда одновременно запущены сразу несколько Live Activity. Тап по представлению приведет пользователя прямо в ваше приложение.
Ведущее и замыкающее представления
Ведущее (Leading) представление будет отображаться в левой части Dynamic Island. Вы можете отобразить какую-нибудь актуальную информацию из вашего приложения, связанную с Live Activity. Но старайтесь сильно не перегружать информацией это представление.
Замыкающее (Trailing) представление будет отображаться с правой стороны Dynamic Island. Оно работает точно так же, как ведущее представление, которое вы можете наблюдать на изображении, где представлен пример Live Activity для отслеживания Formula1, над которым мы будем работать в этом примере.
Расширенное представление
Расширенное (Expanded) представление отображается если пользователь зажимает Live Activity. Оно используется для отображения дополнительной информации об Activity. В нашем примере мы будем держать пользователя в курсе, какой гонщик лидирует, а какой движется последним. Как вы можете видеть на изображении ниже, Макс уже на первом месте, а Латифи на последнем — вполне привычное зрелище, если вы следите за F1 в последнее время.
Этот пример довольно прост — вы можете кастомизировать его, настроив представления в определенных областях расширенного представления, и система сама попытается все правильно соразмерить на основе предоставленных вами представлений. На изображении ниже вы можете увидеть, какие области есть в расширенным представлением.
Представление для экрана блокировки
Мы будем использовать аналогичный пример, чтобы показать, как это выглядит на экране блокировки. Важным моментом здесь является то, что в этом случае мы можем использовать другое представление. Это позволяет нам повторно использовать существующий пользовательский интерфейс или создавать новые представления в зависимости от ситуации для Dynamic Island и Live Activity отдельно. Ниже представлен пример, демонстрирующий это.
Создаем наше первое Live Activity
Прежде всего, Live Activity доступны в iOS 16.1, и в этом примере я буду использовать Beta Xcode 14.1.
После того, как вы решили, какое приложение вы хотите “оживить”, зайдите в Xcode и в разделе “Targets” найдите знак “+”, где вы сможете добавить расширение с виджетами (Widget Extension).
После того, как вы добавили расширение с виджетами, проследуйте в файл Info.plist вашего приложения, добавьте логический флаг для разрешения “Supports Live Activities” и установите для него значение “YES”.
Теперь мы готовы начинать представление. Во-первых, мы определим FormulaAttributes
, который я упоминал выше. Он соответствует протоколу ActivityAttributes
, который помогает нам определять динамические данные внутри нашей структуры. Убедитесь, что при создании файла FormulaAttributes.swift
вы включаете его как в таргеты и приложения, и виджета.
Прежде чем мы углубимся в код, хочу предупредить, при запуске Activity ваше приложение должно быть на переднем плане (foreground), в то время как вы можете обновить или завершить его можно уже и в фоновом режиме (background).
Запуск Live Activity
Сначала мы добавим кнопку и ссылку на наше Activity внутри ContentView
. Activity будет установлено как опциональное, поскольку мы хотим создать его, когда оно нам понадобится, а ссылку нам нужно сохранить, чтобы мы могли позже обновить правильное Live Activity.
struct ContentView: View {
@State var activity: Activity<FormulaAttributes>?
var body: some View {
VStack(spacing: 20) {
Button(action: { activity = startActivity() }, label: { Text("Start Activity") })
}
}
}
Компилятор жалуется нам, что нет функции startActivity
, поэтому мы добавим его в приватное расширение ContentView
. Функция startActivity
возвращает только что созданное Activity.
private extension ContentView {
func startActivity() -> Activity<FormulaAttributes>? {
var activity: Activity<FormulaAttributes>?
let attributes = FormulaAttributes(lastPlaceDriver: "Nicholas Latifi")
do {
let contentState = FormulaAttributes.ContentState(
driverInFront: "Max Verstappen",
driverTeam: "Red Bull racing"
)
activity = try Activity<FormulaAttributes>.request(attributes: attributes, contentState: contentState)
} catch {
print(error.localizedDescription)
}
return activity
}
}
Теперь, когда мы рассмотрели реализацию нашего Live Activity, мы можем приступить к изучению обновлений Activity.
Обновление Live Activity
Добавьте еще одну кнопку внутри ContentView
, точно такую же, как мы добавили, чтобы запустить Activity и вызвать updateActivity
. Внутри приватного расширения ContentView
добавьте следующую функцию:
func updateActivity() {
Task {
let contentState = FormulaAttributes.ContentState(
driverInFront: "not Lewis Hamilton",
driverTeam: "Mercedes"
)
await activity?.update(using: contentState)
}
}
Функция получит ссылку, которую мы сохранили для нашей Activity, и вызовет функцию .update(using:)
с новым содержимым. Она обернута в Task
, так как это асинхронная функция.
В этом примере показано, как обновить наше Live Activity из приложения, но, как упоминалось ранее, вы также можете сделать это с помощью push-уведомлений. Код будет немного отличаться от нашего примера — вам нужно будет запустить Live Activity, указав параметр pushType
. Чтобы оно могло получать уведомления, вам нужно будет отправить pushToken
из вашей Live Activity на сервер.
Завершение Live Activity
Завершение Live Activity также можно выполнить из приложения и с помощью уведомлений. Как я уже упоминал ранее, убедитесь, что вы обновили Live Activity актуальной информацией, прежде чем завершить его.
Добавьте еще одну кнопку в ContentView
, которая будет ссылаться на функцию endActivity
. Внутри приватного расширения ContentView
добавьте следующую функцию:
func endActivity() {
Task {
for activity in Activity<FormulaAttributes>.activities {
await activity.end(dismissalPolicy: .immediate)
}
}
}
Функция endActivity
проходит через все запущенные Live Activity с FormulaAttributes
и завершает их, указав, что они должны немедленно удалить Activity с экрана блокировки. Как и функция update(using:)
, end(dismissalPolicy:)
также является асинхронной функцией.
Пользовательский интерфейс Live Activity
Поскольку в этом примере мы используем только виджет Live Activity, мы снабдим его аннотацией @main@main
. Если вы используете больше виджетов, сгруппируйте их в WidgetBundle
. В приведенном ниже коде показан пользовательский интерфейс для виджета Formula1, который использовался для создания предыдущих скриншотов.
@main
struct FormulaActivityWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: FormulaAttributes.self) { context in
// Создайте здесь представление, которое будет отображаться на экране блокировки и на главном экране в виде баннера
// Для устройств, которые не поддерживают Dynamic Island
} dynamicIsland: { context in
DynamicIsland {
// Это содержимое будет отображаться, когда пользователь расширяет Island
DynamicIslandExpandedRegion(.center) {
VStack {
Text("Driver in front is \(context.state.driverInFront) ????")
Text("Last place driver is \(context.attributes.lastPlaceDriver)")
}
}
} compactLeading: {
// Это представление отображается в левой части Dynamic Island
Text("????")
} compactTrailing: {
// Это представление отображается в правой части Dynamic Island
Image(systemName: "timer")
} minimal: {
// Это представление будет отображаться, когда одновременно запущено несколько Activity
Text("????")
}
}
}
}
Внутри виджета мы указываем ActivityConfiguration
и какие атрибуты он будет использовать. Сначала мы настроим представления на Dynamic Island, а потом добавим представление экран блокировки.
Чтобы упростить пример, я использовал только центральную область расширенного представления, внутри которого собираюсь установить некоторые данные. После этого будет замыкание, описывающее, какую часть Dynamic Island оно покрывает, чтобы вы могли разместить любое представление, который вы хотите, чтобы он отображал.
Теперь мы можем настроить экран блокировки и домашний баннер. Вот представление экрана блокировки, которое будет иметь ссылку на контекст из наших атрибутов, чтобы его можно было правильно обновить.
struct LockScreenLiveActivityView: View {
let context: ActivityViewContext<FormulaAttributes>
var body: some View {
VStack {
Text(“Driver in front is \(context.state.driverInFront) ????“)
Spacer()
Text(“Last place driver is \(context.attributes.lastPlaceDriver) ????“)
}
.activitySystemActionForegroundColor(.white)
.activityBackgroundTint(.cyan)
}
}
После того, как вы создали представление для экрана блокировки, инициализируйте его внутри замыкания для ActivityConfiguration
. Вот и все, теперь вы можете запустить приложение и протестировать его.
Добавление диплинка внутри расширенного представления
Поскольку Live Activity — это виджет, он может создавать глубокие ссылки (DeepLink) на ваше приложение. Каждое представление Live Activity может вести к разным местам внутри вашего приложения, но Apple по-прежнему рекомендует, чтобы ведущее и замыкающее представления вели к одному и тому же месту. DeepLinking — отличный инструмент для добавления в расширенное представление, где у вас может быть несколько представлений, некоторые из которых могут перемещаться туда, куда вам нужно.
Вещи, на которые следует обратить внимание в
Live Activity, можно отключить в настройках приложении. Вы можете проверять, сделано ли это, и предоставить пользователю грамотное объяснение, сообщив ему, что он упускает.
Обязательно уделите внимание обработке ошибок. Пользователи могут запускать несколько Live Activity одновременно, да вы и сами можете достичь состояния, когда ваше приложение запускает слишком много Activity. В ваших же интересах убедиться, что такой сценарий исключен.
Система игнорирует любые модификаторы анимации при определении пользовательского интерфейса Live Activity, но вы можете изменить анимацию, которую Apple использует, чтобы создать более уникальный опыт.
Live Activity будет отображаться в темной цветовой схеме, когда включена функция Always-On Retina
Используйте обновления приложений вместе с обновлениями push-уведомлений. Иногда пользователь может не получить push-уведомление из-за отсутствия подключения к интернету или из-за того, что Live Activity завершилась.
При использовании push-уведомлений учитывайте, что система может ограничить ваши push-уведомления, и пользователь может не получить их, если вы отправили их слишком много. Это очень важно, если вы создаете Live Activity для такой темы, как Formula1, где у вас будет очень много обновлений. Один из способов повысить шансы отправки push-уведомления — седлать его низкоприоритетным, но никто не может гарантировать, что пользователь его получит.
Система может отображать Live Activity даже на устройствах, которые не поддерживают Dynamic Island в виде баннера на главном экране, но только в том случае, если приложение определяет, что обновление достаточно важно, чтобы отвлекать на себя пользователя.
Финишная черта
Live Activity — это новый способ мгновенного отображения информации. Анимации и различные представления делают Live Activity уникальными среди других виджетов. Адаптируйте Live Activity к потребностям вашего приложения и сделайте их уникальным, но приятным для пользователей опытом.
PS: Нам, вероятно, даже не нужно обновлять наше Live Activity, так как Макс, скорее всего, в очередной раз выиграл.
Материал подготовлен в преддверии старта курса "iOS Developer. Professional"
Недавно прошел открытый урок «Пример реализации технологии Flux на SwiftUI», на котором мы рассмотрели некоторые проблемы и сложности реализации MVVM на SwiftUI, а также попробовали применить Flux архитектуру для реализации небольшого приложения. Если интересно, запись вебинара можно посмотреть по ссылке.
Larvis
Хорошая статья для введения в курс. т.е. получается, можно создать один Live Activity для "острова" и оно будет более-менее красиво отображаться на остальных устройствах?
Заложено ли в систему ограничение на количество этих самых Live Activity? (для всех приложений, а не для одного).