Начало работы с Dynamic Island и Live Activities в iOS 16.1

от автора

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

Документация Apple рассказывает, как начать работу с Dynamic Island, динамическим островом. С ним можно анимированно показывать информацию вокруг области выреза фронтальной камеры, которую мы привыкли называть «чёлкой».

В этой статье мы рассмотрим пример базовой работы с размещением контента в 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.

Очевидно, здесь можно передать для отрисовки что-то более сложное, чем строки. Что и в каком именно месте будет отображаться, выбирать вам.

Динамический остров может быть полезен, когда вы хотите показать обновленную информацию о текущей задаче. Например, обновления в реальном времени для спортивного или навигационного приложения, информация обратного отсчета приложения таймера, элементы управления музыкой для музыкальных приложений, данные о загрузке или скачивания файла и т.д.

(прим. переводчиков)


ссылка на оригинал статьи https://habr.com/ru/company/kts/blog/692574/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *