Разработка Dynamic Island интеграции для iOS
Dynamic Island — интерактивная зона вокруг Face ID вырезки на iPhone 14 Pro и новее. Технически это часть Live Activities API: без Live Activity Dynamic Island не работает. Но спроектировать хорошую Dynamic Island интеграцию — отдельная задача с конкретными UX-ограничениями и техническими тонкостями.
Три состояния Dynamic Island
Minimal — когда на устройстве несколько активных Live Activities одновременно. Ваша Activity конкурирует с другими. Система показывает одну иконку справа или слева от «острова». Контент — одна иконка или очень короткий текст. Пользователь нажимает — переходит в Compact или Expanded.
Compact — стандартное свёрнутое состояние. Две зоны: compactLeading (левая часть вырезки) и compactTrailing (правая). Доступная ширина — примерно 100pt на каждую зону, высота — 36pt. Не пытайтесь вместить много: иконка + короткое число — максимум.
Expanded — долгий тап пользователя. Раскрывается до большой карточки. Четыре зоны: .leading, .trailing, .center, .bottom. Bottom-зона самая высокая — туда идёт основной контент.
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
// Статус иконка
Image(systemName: orderStatusIcon)
.font(.title2)
.foregroundColor(.orange)
}
DynamicIslandExpandedRegion(.trailing) {
// Время
Label("\(minutesLeft) мин", systemImage: "clock")
.font(.caption)
}
DynamicIslandExpandedRegion(.center) {
// Центральный контент
Text(restaurantName)
.font(.headline)
.lineLimit(1)
}
DynamicIslandExpandedRegion(.bottom) {
// Основное действие
HStack {
ProgressView(value: deliveryProgress)
.tint(.orange)
Text(statusText)
.font(.subheadline)
}
.padding(.horizontal)
}
} compactLeading: {
Image(systemName: "bag.fill").foregroundColor(.orange)
} compactTrailing: {
Text("\(minutesLeft)м").font(.caption2).bold()
} minimal: {
Image(systemName: "bag.fill")
}
.keyframeAnimator(initialValue: AnimationValues()) { content, value in
content.scaleEffect(value.scale)
} keyframes: { _ in
KeyframeTrack(\.scale) {
LinearKeyframe(1.0, duration: 0.1)
SpringKeyframe(1.1, duration: 0.3)
SpringKeyframe(1.0, duration: 0.2)
}
}
keyframeAnimator (iOS 17+) — анимация при обновлении ContentState. Пульсирующая иконка при смене статуса — именно так. Без анимации обновление просто «мигает».
Переход из Dynamic Island в приложение
Нажатие на Dynamic Island в Expanded состоянии открывает приложение. widgetURL(_:) в View задаёт URL для deep link. В приложении — onContinueUserActivity(_:perform:) или onOpenURL в SwiftUI.
Важно: разные зоны Expanded могут вести на разные deep link URLs. Левая зона — в карту с курьером, нижняя — в детали заказа.
Анимации при появлении и обновлении
При старте Live Activity Dynamic Island «вырастает» с анимацией. При обновлении ContentState — переход между состояниями анимируется системой, но можно добавить кастомную анимацию через withAnimation в View.
Смена compactLeading/compactTrailing контента при обновлении: система применяет crossfade. Для более контролируемого перехода — ContentTransition.numericText() для числовых значений (время, счёт), ContentTransition.identity для замены контента без анимации.
Что нельзя и что часто забывают
Нельзя запустить без активного Activity<T> — Dynamic Island существует только пока Live Activity жива. Нет Live Activity — нет Dynamic Island.
Нельзя показывать произвольный контент в Minimal без Activity. Нельзя управлять позицией (слева/справа в Minimal) — система решает сама.
Compact-зоны не интерактивны сами по себе — нажатие на Compact открывает Expanded, затем нажатие в Expanded ведёт по widgetURL. Сделать кнопку прямо в Compact не получится.
Тестирование на симуляторе: Dynamic Island симулируется как «плоская» зона, анимации упрощены. Финальный вид и поведение — только на реальном iPhone 14 Pro/Pro Max.
Срок: 3-5 дней при наличии готовой Live Activity. Если Live Activity ещё нет — нужно добавить её разработку. Стоимость рассчитывается после анализа требований.







