Получайте обновления в режиме реального времени на экране блокировки и на динамическом острове.

API support iOS 16.1+ Beta iPadOS 16.1+ Beta Mac Catalyst 16.1+ Beta
The final code has been updated for the iPhone 14 series that supports Dynamic Island.

Если вы являетесь строгим разработчиком, следящим за инновациями Apple, вы, должно быть, недавно слышали о «Отображение данных в реальном времени на экране блокировки и на динамическом острове с живыми действиями».

Apple объявила об этом несколько недель назад, и тогда я услышал об этом новости.

Apple предоставляет ему новый фреймворк под названием ActivityKit. С помощью ActivityKit вы можете делиться оперативными обновлениями из своего приложения в виде живых действий на экране блокировки iPhone и в Dynamic Island.

Live Activity используют функциональность WidgetKit и SwiftUI для своего пользовательского интерфейса на экране блокировки.

Роль ActivityKit заключается в управлении жизненным циклом каждого Live Activity: вы используете его API для запроса, обновления и завершения Live Activity. - Яблоко

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

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

Этого достаточно для примеров. Хорошо, давайте начнем кодирование Live Activity.

Как закодировать приложение Live Activity

Нам нужно создать виджет для настройки ActivityKit. Это похоже на расширения виджетов.

Мы будем использовать SwiftUI и WidgetKit для создания пользовательского интерфейса Live Activity. Live Activity работает как Widget Extension и обеспечивает совместное использование кода между вашими виджетами и Live Activity.

Однако Live Activity использует другой механизм для получения обновлений по сравнению с виджетами.

Вместо использования механизма временной шкалы Live Activity получают обновленные данные из вашего приложения с помощью ActivityKit или путем получения удаленных push-уведомлений. - Яблоко

Важные примечания

Live Activity доступны только на iPhone.

Live Activity и ActivityKit не будут включены в первоначальную общедоступную версию iOS 16, но будут общедоступны в обновлении позже в этом году (запланировано с iOS 16.1). Как только они станут общедоступными, вы сможете отправить свои приложения с Live Activity в App Store.

Я собираюсь создать приложение для доставки продуктов. А затем я собираюсь создать виджет ActivityKit для живых действий процесса доставки.

Если вы уже предлагаете виджеты в своем приложении, вы можете добавить код пользовательского интерфейса для Live Activity в существующее расширение виджета и, возможно, сможете повторно использовать код между вашими виджетами и Live Activity.

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

Создание приложения

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

  • Следуйте инструкциям, чтобы создать одно приложение со SwiftUI, а затем назовите его GroceryDeliveryApp.

Xcode -> Файл -> Создать -> Проект -> Приложение

  • Добавьте следующий ключ в info.plist вашего приложения и установите значение YES.

NSSupportsLiveActivities

Этот ключ будет поддерживать приложение для Live Activity.

  • После настройки приложения нам нужно создать расширение виджета.

Следуйте инструкциям по навигации, чтобы создать расширение виджета, а затем назовите его DeliveryTrackWidget.

Навигатор проектов > Выберите проект > Добавить цель из списка целей > Расширение виджета

  • Добавьте следующий ключ к расширению info.plist и установите для него значение YES.

NSSupportsLiveActivities

Этот ключ будет поддерживать расширение для Live Activity.

После того, как вы создали свое приложение и расширение виджета, вы можете спроектировать его по своему усмотрению.

Прежде всего, нам нужно создать файл ActivityAttributes.

Мы используем ActivityAttributes для идентификации Live Activity. Затем мы используем ContentState для указания динамического содержимого. Я создал структуру ActivityAttributes со следующим кодом:

import SwiftUI
import ActivityKit

struct GroceryDeliveryAppAttributes: ActivityAttributes {
    public typealias LiveDeliveryData = ContentState

    public struct ContentState: Codable, Hashable {
        var courierName: String
        var deliveryTime: Date
    }
    var numberOfGroceyItems: Int
}

Затем нам нужно создать запрос Live Activity с классом Activity. Класс Activity принимает общий тип для структуры ActivityAttributes.
Обратите внимание: если вы хотите обновить или завершить это действие, вы должны передать здесь значение pushType.

Значение токена pushType представляет собой обновление вашей активности на конкретном устройстве iOS через систему Apple Push Service (APS). Ваше приложение получит push-токен после запроса активности для настройки протокола APS, после чего вы сможете отправлять полезные данные обновления активности через APS.

Обратите внимание, что размер обновленных динамических данных как для обновлений ActivityKit, так и для обновлений удаленных push-уведомлений не может превышать 4 КБ.

Я создал структуру ActivityAttributes со следующим кодом:

let attributes = GroceryDeliveryAppAttributes(numberOfGroceyItems: 12)
let contentState = GroceryDeliveryAppAttributes.LiveDeliveryData(courierName: "Mike", deliveryTime: .now + 120)
do {
    let _ = try Activity<GroceryDeliveryAppAttributes>.request(
        attributes: attributes,
        contentState: contentState,
        pushType: nil)
} catch (let error) {
    print(error.localizedDescription)
}

Убедитесь, что живые действия доступны. Не забывайте, что пользователи могут отключить Live Activity для приложения в приложении «Настройки» на iPhone.

Если вы хотите определить, доступны ли живые действия и пользователь разрешил вашему приложению использовать живые действия, вы можете сделать это с помощью areActivitiesEnabled и activityEnablementUpdates.

Кроме того, чтобы наблюдать за состоянием текущей Live Activity, вы можете использовать activityStateUpdates.

Создав трансляцию, вы можете обновить или завершить ее через ContentState.

Я обновил и завершил Live Activity следующим кодом:

let updatedStatus = GroceryDeliveryAppAttributes.LiveDeliveryData(courierName: "Adam",
                                                                              deliveryTime: .now + 150)
await activity.update(using: updatedStatus)
await activity.end(dismissalPolicy: .immediate)

Вы можете использовать .after(_ date: Date) вместо .immediate, чтобы завершить трансляцию позже.

Каждое действие имеет pushTokenUpdates для обновления своего содержимого с помощью удаленного push-уведомления. Вы можете использовать данные pushToken для обновления содержимого Live Activity. В этом случае ваше приложение должно быть зарегистрировано в APNS для удаленных push-уведомлений. Если вы новичок в удаленных push-уведомлениях, прочитайте документацию по фреймворку User Notifications.

Полезная нагрузка удаленного push-уведомления должна соответствовать вашей части атрибутов динамических данных. Пример полезной нагрузки push-уведомления должен выглядеть как JSON ниже:

{
    "aps": {
        "timestamp": 1660435557,
        "event": "update",
        "content-state": {
            "courierName": "Adam",
            "deliveryTime": 1660435557
        }
    }
}

Требования и ограничения Live Activity

Live Activity может быть активна до восьми часов, если ваше приложение или пользователь не завершит ее. После этого лимита система автоматически завершает его. Когда Live Activity заканчивается, система немедленно удаляет его из Dynamic Island. Тем не менее, Live Activity остается на экране блокировки до тех пор, пока пользователь не удалит ее, или еще до четырех часов, прежде чем система удалит ее.

В результате Live Activity остается на экране блокировки не более двенадцати часов.

Live Activity подходит для экрана блокировки и динамического острова. Экран блокировки отображается на всех устройствах с iOS 16.1 и более поздних версий.

Devices that support the Dynamic Island display Live Activities using the following views: 
A compact leading view,
A compact trailing view, 
A minimal view, 
An expanded view

Расширенное представление появляется, когда человек касается и удерживает компактное или минимальное представление в Dynamic Island, а также при обновлении Live Activity. На разблокированном устройстве, которое не поддерживает Dynamic Island, расширенное представление отображается как баннер для обновлений Live Activity.
Чтобы система могла отображать ваши Live Activity в каждой позиции, вы должны поддерживать все представления.

Когда мы обновляем Live Activity, ActivityConfiguration возвращает ActivityAttributes для обновления пользовательского интерфейса виджета. ActivityConfiguration предоставляет контекст для доступа к динамическим данным (ContentState) и статическим данным файла ActivityAttributes.

Я создал пользовательский интерфейс Live Activity с помощью кода ниже:

//
//  DeliveryTrackWidget.swift
//  DeliveryTrackWidget
//
//  Created by Batikan Sosun on 13.08.2022.
//

import ActivityKit
import WidgetKit
import SwiftUI

@main
struct Widgets: WidgetBundle {
    var body: some Widget {
        if #available(iOS 16.1, *) {
            GroceryDeliveryApp()
        }
    }
}

@available(iOSApplicationExtension 16.1, *)
struct GroceryDeliveryApp: Widget {
    
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: GroceryDeliveryAppAttributes.self) { context in
            LockScreenView(context: context)
        } dynamicIsland: { context in
            DynamicIsland {
                DynamicIslandExpandedRegion(.leading) {
                    dynamicIslandExpandedLeadingView(context: context)
                 }
                 
                 DynamicIslandExpandedRegion(.trailing) {
                     dynamicIslandExpandedTrailingView(context: context)
                 }
                 
                 DynamicIslandExpandedRegion(.center) {
                     dynamicIslandExpandedCenterView(context: context)
                 }
                 
                DynamicIslandExpandedRegion(.bottom) {
                    dynamicIslandExpandedBottomView(context: context)
                }
                
              } compactLeading: {
                  compactLeadingView(context: context)
              } compactTrailing: {
                  compactTrailingView(context: context)
              } minimal: {
                  minimalView(context: context)
              }
              .keylineTint(.cyan)
        }
    }
    
    
    //MARK: Expanded Views
    func dynamicIslandExpandedLeadingView(context: ActivityViewContext<GroceryDeliveryAppAttributes>) -> some View {
        VStack {
            Label {
                Text("\(context.attributes.numberOfGroceyItems)")
                    .font(.title2)
            } icon: {
                Image("grocery")
                    .foregroundColor(.green)
            }
            Text("items")
                .font(.title2)
        }
    }
    
    func dynamicIslandExpandedTrailingView(context: ActivityViewContext<GroceryDeliveryAppAttributes>) -> some View {
        Label {
            Text(context.state.deliveryTime, style: .timer)
                .multilineTextAlignment(.trailing)
                .frame(width: 50)
                .monospacedDigit()
        } icon: {
            Image(systemName: "timer")
                .foregroundColor(.green)
        }
        .font(.title2)
    }
    
    func dynamicIslandExpandedBottomView(context: ActivityViewContext<GroceryDeliveryAppAttributes>) -> some View {
        let url = URL(string: "LiveActivities://?CourierNumber=87987")
        return Link(destination: url!) {
            Label("Call courier", systemImage: "phone")
        }.foregroundColor(.green)
    }
    
    func dynamicIslandExpandedCenterView(context: ActivityViewContext<GroceryDeliveryAppAttributes>) -> some View {
        Text("\(context.state.courierName) is on the way!")
            .lineLimit(1)
            .font(.caption)
    }
    
    
    //MARK: Compact Views
    func compactLeadingView(context: ActivityViewContext<GroceryDeliveryAppAttributes>) -> some View {
        VStack {
            Label {
                Text("\(context.attributes.numberOfGroceyItems) items")
            } icon: {
                Image("grocery")
                    .foregroundColor(.green)
            }
            .font(.caption2)
        }
    }
    
    func compactTrailingView(context: ActivityViewContext<GroceryDeliveryAppAttributes>) -> some View {
        Text(context.state.deliveryTime, style: .timer)
            .multilineTextAlignment(.center)
            .frame(width: 40)
            .font(.caption2)
    }
    
    func minimalView(context: ActivityViewContext<GroceryDeliveryAppAttributes>) -> some View {
        VStack(alignment: .center) {
            Image(systemName: "timer")
            Text(context.state.deliveryTime, style: .timer)
                .multilineTextAlignment(.center)
                .monospacedDigit()
                .font(.caption2)
        }
    }
}





@available(iOSApplicationExtension 16.1, *)
struct LockScreenView: View {
    var context: ActivityViewContext<GroceryDeliveryAppAttributes>
    var body: some View {
        VStack(alignment: .leading) {
            HStack {
                VStack(alignment: .center) {
                    Text(context.state.courierName + " is on the way!").font(.headline)
                    Text("You ordered \(context.attributes.numberOfGroceyItems) grocery items.")
                        .font(.subheadline)
                    BottomLineView(time: context.state.deliveryTime)
                }
            }
        }.padding(15)
    }
}

struct BottomLineView: View {
    var time: Date
    var body: some View {
        HStack {
            Divider().frame(width: 50,
                            height: 10)
            .overlay(.gray).cornerRadius(5)
            Image("delivery")
            VStack {
                RoundedRectangle(cornerRadius: 5)
                    .stroke(style: StrokeStyle(lineWidth: 1,
                                               dash: [4]))
                    .frame(height: 10)
                    .overlay(Text(time, style: .timer).font(.system(size: 8)).multilineTextAlignment(.center))
            }
            Image("home-address")
        }
    }
}

Ссылка: Документация Apple

Обратите внимание, что система может обрезать Live Activity, если его высота превышает 160 пунктов.

Вот ссылка на весь код на Github.

Спасибо за прочтение.

Want to Connect?
Let’s connect on Twitter @batikansosun.