Как стать автором
Обновить
101.75
KTS
Создаем цифровые продукты для бизнеса

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

Время на прочтение7 мин
Количество просмотров8K
Автор оригинала: Rob Sturgeon

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

Документация 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-разработке:

Теги:
Хабы:
Всего голосов 13: ↑13 и ↓0+13
Комментарии4

Публикации

Информация

Сайт
kts.tech
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия