Обновлено: .

Dynamic Island полезен, когда отвечает на короткий вопрос: когда приедет машина, сколько осталось до конца таймера, что происходит с тренировкой. Чтобы сделать такую функцию надёжной, нужны не только компактный макет, но и ActivityKit, widget extension, корректные обновления, обработка действий и понятное завершение. Здесь — полный путь от продуктового сценария до проверки реализации.

Человек заказал еду и открыл рабочую переписку. В верхней части экрана ему достаточно увидеть «4 мин». При раскрытии — понять, что курьер у входа со двора. На блокировке — узнать тот же заказ. После доставки статус должен завершиться. Именно эту цепочку мы проектируем: один процесс, несколько представлений и ни одного обещания, которое данные уже не подтверждают.

Проверено по документации Apple 5 ноября 2026 года. Это практическое руководство и авторские схемы, а не отчёт о тестировании всех iPhone. Примеры Swift показывают отдельные части интеграции; они не являются собранным и подписанным приложением. Доступность каждого API нужно сверять с Xcode SDK и deployment target проекта.

1. Dynamic Island, Live Activity и виджет — три разных понятия

Dynamic Island — место отображения. Live Activity — представление текущей активности. ActivityKit — механизм управления её жизнью. Интерфейс создают с помощью WidgetKit и SwiftUI в расширении приложения. Обычный виджет, хотя и использует тот же WidgetKit, обновляется по другому контракту: его timeline нельзя подставить вместо жизненного цикла Live Activity. Это различие закреплено в руководстве Apple по интеграции Live Activities.

Запись диктофона с волной звука и временем в Dynamic Island на iPhone
В руководстве Apple показано, как запись диктофона остаётся видна в Dynamic Island поверх домашнего экрана. Apple.
Live Activity доставки на экране блокировки iPhone: три товара и прибытие через восемь минут
Пример из Human Interface Guidelines Apple: та же задача доставки получает отдельное представление на экране блокировки. Apple.
МеханизмЗа что отвечаетПример
ActivityKitСоздать, обновить, завершить активностьДоставка от начала отслеживания до финала
WidgetKit + SwiftUIОписать представления и поддерживаемые взаимодействияКраткий ETA и расширенная карточка
Dynamic IslandСистемная область показа на поддерживаемом устройствеКомпактное, минимальное, раскрытое состояние
Обычный виджетРегулярная информация и быстрый входПлан на день, ближайшее событие

Отсюда важное следствие: поддержка iPhone без островка не является бесполезной работой. Экран блокировки остаётся самостоятельным контекстом. И наоборот: рисунок капсулы внутри приложения не подключает продукт к системному интерфейсу.

2. Сначала договоритесь, что именно человек отслеживает

Для доставки задайте момент начала наблюдения, смену этапов и финал. «Заказ принят» может ещё не требовать постоянного внимания. «Курьер в пути» уже отвечает на повторяющийся вопрос. «Курьер ждёт» требует заметного изменения. «Доставлено» завершает процесс; карточка не должна превращаться в предложение заказать ещё.

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

AI-генерация тоже не получает автоматического права на островок. Если результат можно спокойно получить позже, уведомление о готовности и экран истории обычно решают задачу. Если пользователь осознанно следит за короткой текущей работой, оцените live-представление по её реальной полезности. Никогда не показывайте выдуманный процент ради красивой анимации.

Перед макетом запишите одну фразу: «Пока происходит процесс, человек хочет знать ответ и иногда выполнить действие; представление исчезает при условии». Если последняя часть отсутствует, скорее всего, выбран не тот механизм.

3. Четыре основных макета и один информационный приоритет

Четыре авторских представления доставки: compact с четырьмя минутами, minimal со значком, expanded с контекстом курьера и Lock Screen с этапом заказа.
Схема информационной иерархии. Условные макеты, не точные размеры и не скриншоты iOS.

В компактном состоянии оставьте главный показатель. Minimal должен оставаться узнаваемым при соседстве с другой активностью. Expanded может добавлять контекст и действие. На экране блокировки нужны достаточные сведения, чтобы понять задачу без открытия приложения. Размеры, зоны и правила раскрытия задаёт система; их следует сверять с Human Interface Guidelines для Live Activities.

Развёрнутая Dynamic Island с заказом, временем прибытия и кнопкой связи с курьером
Официальный пример Apple крупным планом: в развёрнутой Dynamic Island появляются детали доставки и действие «Связаться». Красным обозначена область камеры в учебной иллюстрации Apple. Apple Human Interface Guidelines.

Например: компактно — «4 мин»; минимально — символ доставки; раскрыто — «Курьер у входа со двора»; на блокировке — этап и связь с конкретным заказом. Не уменьшайте один большой экран до четырёх размеров. Выбирайте информацию заново для каждого контекста.

Учитывайте ограниченную ширину островка, несколько одновременно активных процессов, крупный текст, длинные переводы и VoiceOver. Число «4» без доступного имени неоднозначно. Подпись «Курьер прибудет примерно через четыре минуты» сообщает смысл. Секретные детали заказа не должны становиться видны только потому, что телефон лежит на столе.

4. Подготовка проекта и модель данных

Начните с существующего iOS target. Добавьте или используйте Widget Extension, включите конфигурацию Live Activity в набор виджетов расширения и установите NSSupportsLiveActivities = YES у приложения. Проверьте подпись, идентификаторы, целевые версии и доступность общего типа данных для приложения и расширения. Сайт или JavaScript-обёртка сами по себе такой target не создают.

ActivityAttributes разделяет постоянные атрибуты и изменяемый ContentState. Номер заказа остаётся постоянным; этап и прогноз прибытия меняются. В минимальном примере не передаём адрес, персональные сведения, изображения и полную серверную модель:

import ActivityKit

struct DeliveryAttributes: ActivityAttributes {
    struct ContentState: Codable, Hashable {
        var phase: String
        var etaMinutes: Int?
    }
    let orderID: String
}

etaMinutes может быть неизвестен. Значение nil здесь честнее нуля: «нет прогноза» не означает «курьер уже приехал». В рабочем приложении этапы лучше описывать согласованным набором значений, чтобы backend и клиент не расходились в смысле.

Проверяйте ActivityAuthorizationInfo().areActivitiesEnabled. Настройка обычных уведомлений и разрешение Live Activities не взаимозаменяемы. Изменения можно наблюдать через activityEnablementUpdates; это описано в API настроек ActivityKit. Даже после положительной проверки создание может завершиться ошибкой — например, из-за системных ограничений.

enum DeliveryActivityError: Error { case disabled }

@available(iOS 16.2, *)
func startDelivery(orderID: String) throws
    -> Activity<DeliveryAttributes> {
    guard ActivityAuthorizationInfo().areActivitiesEnabled else {
        throw DeliveryActivityError.disabled
    }
    let state = DeliveryAttributes.ContentState(
        phase: "on_the_way", etaMinutes: 4
    )
    return try Activity<DeliveryAttributes>.request(
        attributes: DeliveryAttributes(orderID: orderID),
        content: ActivityContent(
            state: state,
            staleDate: .now.addingTimeInterval(120)
        ),
        pushType: nil
    )
}

Функция намеренно возвращает ошибку вызывающему коду. Продукт должен показать результат создания в своём обычном интерфейсе. Пример использует ActivityContent, доступный с iOS 16.2, и локальное обновление без push. Для серверного варианта потребуется отдельная настройка, описанная ниже.

5. Как связать данные с Dynamic Island

В ActivityConfiguration задаётся обычное представление, а в DynamicIsland — компактные области и раскрытый вид. Следующий фрагмент показывает именно структуру. Вид DeliverySummary — компонент вашего приложения, который нужно реализовать; он не является классом Apple.

ActivityConfiguration(for: DeliveryAttributes.self) { context in
    DeliverySummary(state: context.state, isStale: context.isStale)
} dynamicIsland: { context in
    DynamicIsland {
        DynamicIslandExpandedRegion(.bottom) {
            DeliverySummary(state: context.state, isStale: context.isStale)
        }
    } compactLeading: {
        Image(systemName: "shippingbox")
    } compactTrailing: {
        if !context.isStale, let eta = context.state.etaMinutes {
            Text("\(eta) мин")
        } else {
            Text("—")
        }
    } minimal: {
        Image(systemName: "shippingbox")
    }
}

В реальном коде DeliverySummary должен явно обрабатывать isStale и отсутствие ETA. Для перехода добавьте widgetURL, ведущий к нужному заказу. После открытия проверьте текущий аккаунт и доступ к объекту: публичная ссылка не является авторизацией. Не отправляйте человека на главный экран искать тот заказ, по которому он только что нажал.

6. Кнопки: App Intents и подтверждённый результат

Для поддерживаемых интерактивных представлений используют Button(intent:) и Toggle с App Intent. Нужный intent обращается к той же команде предметной области, что и основной экран. Apple объясняет контекст выполнения в инструкции по интерактивным виджетам и Live Activities; для операций с активностью предусмотрен LiveActivityIntent.

У паузы тренировки цепочка такова: нажатие → проверка текущей сессии → команда остановить запись на паузе → подтверждение → новое состояние. Если команда идёт на другое устройство или сервер, это не мгновенная локальная смена надписи. Повторное нажатие не должно случайно возобновить тренировку: команда «установить паузу» надёжнее слепого «переключить состояние».

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

7. Серверные обновления: три токена, которые нельзя путать

Когда данные приходят с сервера, подключите Push Notifications capability и создавайте активность с pushType: .token. Затем наблюдайте activity.pushTokenUpdates: токен появляется асинхронно и может измениться. Backend связывает его с конкретной активностью и аккаунтом. Не сохраняйте его в открытых логах.

ТокенНазначениеОшибка, которой избежать
Обычный device token APNsОбычные удалённые уведомления приложенияИспользовать его вместо ActivityKit update token
Update token активностиОбновить или завершить конкретную Live ActivityПривязать один токен ко всем заказам
Push-to-start tokenУдалённо начать новую активность на поддерживаемом runtimeОтправить туда обычное обновление или игнорировать ротацию

Для обновления конкретной активности запрос APNs использует apns-push-type: liveactivity и topic вида <app-bundle-id>.push-type.liveactivity. Содержимое content-state обязано соответствовать Swift-модели. Вот авторский пример формирования обновления на JavaScript:

const now = Math.floor(Date.now() / 1000);
const payload = {
  aps: {
    timestamp: now,
    event: "update",
    "content-state": { phase: "on_the_way", etaMinutes: 4 },
    "stale-date": now + 120
  }
};

Здесь 120 секунд — выбранное для учебного заказа окно актуальности, а не лимит Apple. На сервере время и срок достоверности нужно брать из логики продукта. Значения времени в конверте APNs используют Unix seconds; это не повод кодировать любой Swift Date внутри собственной модели по той же схеме. Проверяйте реальное декодирование.

Полный контракт start/update/end, приоритетов и каналов описан в документации ActivityKit push. Для общего спортивного события возможен broadcast-канал на поддерживаемых версиях; персональные заказы нельзя бездумно объединять в общую аудиторию. Frequent Updates — отдельная настройка и бюджет, а не обещание обновления каждую секунду. Принятие запроса APNs не доказывает ни отображение, ни просмотр.

8. Актуальность, завершение и реальные ограничения

Локальное подтверждённое изменение передаётся существующему объекту активности через update. В этом примере нулевой ETA допустим, потому что известно: курьер уже ждёт, а не потому, что прогноз потерян.

let nextState = DeliveryAttributes.ContentState(
    phase: "courier_waiting", etaMinutes: 0
)
await activity.update(
    ActivityContent(
        state: nextState,
        staleDate: .now.addingTimeInterval(120)
    )
)
Жизненный цикл: запуск, актуальный ответ, устаревшие данные, новый подтверждённый ответ и завершение. Удаление карточки не отменяет заказ.
Достоверность данных и видимость карточки меняются независимо. Учебный пример доставки.

Достоверность ответа важнее его непрерывного присутствия. Если обновление ETA не пришло, не продолжайте уверенно обещать прежние четыре минуты. staleDate отмечает момент устаревания; context.isStale позволяет изменить UI. Само поле не пишет текст «Данные устарели» за разработчика и не обновляет прогноз.

Разделяйте четыре вещи: заказ ещё выполняется; данные устарели; карточка скрыта; команда пользователя не подтверждена. Они могут сочетаться. Удаление Live Activity с блокировки не отменяет доставку. После перезапуска приложения сверяйтесь с Activity<DeliveryAttributes>.activities, а не создавайте новую копию каждой задачи.

Для стандартных Live Activities текущая документация указывает до восьми часов активной жизни, затем до четырёх дополнительных часов на экране блокировки, и суммарный бюджет 4 KB на статические и динамические данные. Rendering-расширение не получает произвольный сетевой доступ или обновления геопозиции. Эти ограничения длительности и данных нужно учитывать до выбора сценария.

let finalState = DeliveryAttributes.ContentState(
    phase: "delivered", etaMinutes: nil
)
await activity.end(
    ActivityContent(state: finalState, staleDate: nil),
    dismissalPolicy: .default
)

При завершении передайте финальный контент. Политика .default оставляет системе возможность некоторое время показывать результат на блокировке; .immediate убирает его сразу; .after(...) задаёт ограниченную отсрочку. Выбор должен быть осмысленным: человеку может понадобиться увидеть финальный статус, но бесконечная карточка уже законченной работы создаёт ложное ожидание.

9. Apple Watch, StandBy, Mac и CarPlay

Островок — не единственная точка использования. Для Apple Watch и CarPlay можно подготовить малое представление через supplementalActivityFamilies([.small]) и выбирать содержимое по activityFamily. Руководство по дополнительным представлениям также описывает адаптацию к другим размерам и ограниченной ширине Dynamic Island.

Apple Watch Series 10 со Smart Stack и карточками в watchOS 11
Официальное изображение Apple Watch Series 10 с watchOS 11: Smart Stack выводит карточки на запястье. Это пример конкретной версии системы. Apple.

Важнейшее различие: интерактивность на Apple Watch не означает интерактивность в CarPlay. Система отключает кнопки и переключатели Live Activity в автомобиле. Поэтому фраза «Курьер ждёт» должна оставаться полезной без кнопки рядом. Получение Live Activity на часах или в меню Mac также не создаёт автономное watchOS- или macOS-приложение.

StandBy требует чтения с расстояния; длинный абзац здесь хуже крупного значения и понятного этапа. На часах нужны короткие подписи и удобные цели нажатия. На Mac учитывайте переход к задаче и зависимость от сопряжённого iPhone. Поддержку проверяют по конкретной связке версий и настроек.

В актуальной документации есть и запланированные, и transient-активности. Transient предназначена для короткого взаимодействия в раскрытом островке и заканчивается при выходе из этого контекста; её нельзя подменять обычным фоновым отслеживанием. Перед использованием новых перегрузок request проверяйте availability именно символа и статус релиза SDK, а не только название семейства ActivityKit.

10. Проверка, которая отличает прототип от интеграции

УсловиеОжидаемый результат
Live Activities выключеныОсновная задача доступна; система не обещает островок
Нет Dynamic IslandПроверено нужное представление блокировки
Две активностиПонятна идентичность каждой, нет перепутанных переходов
Данные устарелиETA не выглядит подтверждённым; видна потеря актуальности
Пользователь удалил карточкуЗаказ не отменён; нет принудительного восстановления
Сеть вернулась после финалаСтарое событие не возвращает завершённую задачу
Токен сменился / APNs вернул ошибкуBackend обновил регистрацию и различает доставку и принятие запроса
Активность закончиласьПоказан финал, обновления остановлены, применена dismissal policy

Добавьте отдельные проверки длинных локализаций, крупного текста, VoiceOver, тёмного/тусклого отображения и перехода в приложение после смены аккаунта. Для паузы или отмены проверьте повтор команды и одновременное действие с другого устройства.

Симулятор полезен для макетов, но не подтверждает доставку APNs, поведение конкретного iPhone и передачу на часы. В отчёте разделяйте: собралось; показано в симуляторе; проверено на устройстве; проверено с сервером. Force-quit тестируйте отдельно от обычного сворачивания.

11. Что должно получиться у команды

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

Пример критерия приёмки: «После начала поездки человек видит актуальное прибытие; при потере данных интерфейс перестаёт обещать точный ETA; по нажатию открывается правильная поездка; после завершения обновления прекращаются; удалённая пользователем активность не возникает снова ради внимания». Это гораздо сильнее требования «сделать красивый остров».

Применить руководство с Codex и Claude Code

По результатам этого исследования мы создали скилл Interaction Surfaces для Codex и Claude Code и скилл Adaptive Interfaces. Они помогают проанализировать существующий сайт или приложение, определить доступные API и предложить полезные сценарии: виджеты, текущие активности, часы, дополнительные экраны и адаптацию интерфейса. Вы выбираете, что внедрить, отложить или исключить; затем агент реализует выбранные изменения и проверяет результат в пределах доступных платформ и устройств.

Для работы над своим приложением можно установить скилл Interaction Surfaces из GitHub. В нём есть настройка ActivityKit и WidgetKit, Swift-пример, App Intents, APNs и отдельное руководство по Apple Watch. Английский пакет охватывает всю серию; инструкция по установке и пример вызова находятся в общем обзоре.

Частые вопросы

Можно ли запустить Dynamic Island с обычного сайта?

Сайт может предложить уведомление или ссылку на задачу, но прямого доступа к ActivityKit у него нет. Нужна нативная интеграция приложения; установка PWA не заменяет widget extension.

Нужен ли отдельный сервер для каждого таймера?

Нет. Для известного локального интервала сначала рассмотрите системное отображение времени. Сервер нужен там, где источник изменений удалённый; ежесекундные push ради движения цифр не решают продуктовую задачу.

Можно ли гарантировать, что активность будет в островке?

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

Подходит ли код из статьи для вставки без изменений?

Нет. Он объясняет границы компонентов. Для выпуска нужны реальные модели, представления, обработка ошибок, подпись, маршрутизация, проверка API и тесты проекта.