iPhone Duo — гайд по адаптации

iPhone Duo: адаптация приложения

Разбор шести сессий Apple Developer и Human Interface Guidelines. Каждая фича разделена на дизайн и разработку, со схемами Apple, тайм-кодами видео и общим критерием готовности.

🎨 Design — что рисовать и какие правила ⚙️ Dev — API, код, что сломается Готово, когда — общий DoD

Часть 0 · Контекст устройства

БАЗА Что такое iPhone Duo

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

📎 Референсы

Аппаратно

  • Два дисплея. Внешний (устройство закрыто) — шире и ниже обычного iPhone. Внутренний (открыт) — самый большой экран на iPhone.
  • Шарнир посередине внутреннего дисплея.
  • Две фронтальные камеры. Внешняя — в углу, всегда видима, разворачивается в Dynamic Island. Внутренняя — первая under-display камера на iPhone, невидима пока не активна.
Шесть поз iPhone Duo
Шесть поз. Закрыт · открыт · «книжка» · tabletop («ноутбук») · «палатка» · Split View. Плюс portrait и landscape для первых двух.
Внешний дисплей: шарнир и камера
Внешний дисплей. Камера в углу, видна всегда, вертикально выровнена с контролами.
Внутренний дисплей: шарнир и камера
Внутренний дисплей. Камера под экраном — скрыта, пока не активна.
Home Screen на внешнем дисплее
Home Screen, внешний дисплей
Home Screen на внутреннем дисплее
Home Screen, внутренний дисплей

Size classes — единая система координат

СостояниеHorizontalVertical
Внешний, portraitCompactRegular
Внешний, landscapeCompactCompact
Внутренний (обе ориентации)RegularRegular
Главный тезис Apple. Не делаем отдельный макет под каждую позу. Целей всего две — compact width (внешний) и regular width (внутренний). Остальные позы — следствие правильной resizability.

Три железных правила

  1. Функционал одинаковый во всех позах и на обоих дисплеях. Нельзя привязывать фичу к позе.
  2. Иерархия информации не меняется между дисплеями. На внутреннем можно показать дополнительный уровень той же иерархии, но не другую структуру.
  3. Люди складывают и раскладывают устройство постоянно во время работы. Переходы должны быть предсказуемыми.
🖼 Кадры из сессий Apple (4)
<b>Один макет — десятки размеров.</b> Duo даёт не два, а целый спектр размеров окна: позы, Split View, повороты.
Один макет — десятки размеров. Duo даёт не два, а целый спектр размеров окна: позы, Split View, повороты. ▶ 3:55
<b>Compact width vs regular width.</b> Один и тот же Mail: слева — внешний дисплей (compact), справа — внутренний (regular).
Compact width vs regular width. Один и тот же Mail: слева — внешний дисплей (compact), справа — внутренний (regular). ▶ 4:15
<b>Режим совместимости.</b> Приложения, собранные до iOS 27.0 SDK, работают, но не получают новых возможностей.
Режим совместимости. Приложения, собранные до iOS 27.0 SDK, работают, но не получают новых возможностей. ▶ 0:45
<b>Сборка на iOS 27.1 SDK.</b> Только тогда система включает вертикальные бары, reserved regions и адаптивные контейнеры.
Сборка на iOS 27.1 SDK. Только тогда система включает вертикальные бары, reserved regions и адаптивные контейнеры. ▶ 1:15

Часть 1 · Фундамент

F1 Resizability и size classes

Суть. Приложение должно свободно ресайзиться. Это предпосылка для всего остального — без неё не работает ничего ниже.

📎 Референсы
🎨 Design
  • Проектируем два состояния — compact width и regular width. Всё остальное должно «вытекать» само.
  • Запрещаем себе: фиксированные ширины, собственные брейкпоинты под конкретные девайсы, макеты «под 393pt».
  • Не «переизобретаем» приложение при ресайзе. Существующий макет расширяется под доступное пространство, а не перестраивается во что-то другое.
  • Строим всё на layout margins и safe area insets — это общий язык с разработкой.

Если у вас уже есть iPad-версия или поддержка iPhone Mirroring — 80% работы сделано.

⚙️ Dev
// SwiftUI
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@Environment(\.verticalSizeClass) private var verticalSizeClass

// UIKit
traitCollection.horizontalSizeClass
traitCollection.verticalSizeClass
Что удалить из кодовой базы
  • UIScreen.mainбудет deprecated. На двухэкранном устройстве это неоднозначно. Вместо: environment, traitCollection, scene.bounds, или динамически windowScene.screen.
  • Проверки UIDevice.userInterfaceIdiom для layout-решений.
  • Проверки interface orientation для layout-решений.
  • Фиксированные константы ширины экрана.
⚠️ Внешний дисплей уважает ваши supported orientations. Внутренний — нет.
Concentricity — форма углов на Duo другая
ConcentricRectangle()        // SwiftUI
UICornerConfiguration        // UIKit

UIRequiresFullScreen не спасает: приложение всё равно ресайзится при открытии и закрытии и масштабируется в Split View.

✅ Готово, когда приложение корректно выглядит во всех шести позах в Device Hub без единой проверки idiom, orientation или screen в layout-коде.

🖼 Кадры из сессий Apple (4)
<b>Главный принцип сессии Design for iPhone Duo.</b> Приложение должно свободно ресайзиться — без предположений о конкретном экране.
Главный принцип сессии Design for iPhone Duo. Приложение должно свободно ресайзиться — без предположений о конкретном экране. ▶ 4:35
<b>Size classes — единственный корректный сигнал.</b> Не idiom, не orientation, не размер экрана.
Size classes — единственный корректный сигнал. Не idiom, не orientation, не размер экрана. ▶ 3:15
<b>Ориентации.</b> Внешний дисплей уважает supported orientations, внутренний — нет.
Ориентации. Внешний дисплей уважает supported orientations, внутренний — нет. ▶ 3:55
<b><code>ConcentricRectangle</code>.</b> Радиусы углов на Duo отличаются — не хардкодьте corner radius.
ConcentricRectangle. Радиусы углов на Duo отличаются — не хардкодьте corner radius. ▶ 4:25

F2 Safe area и layout margins

Суть. Из-за вертикальных контролов сбоку safe area на Duo несимметрична. Это источник бага №1.

📎 Референсы
🎨 Design
  • В макетах всегда помечайте отдельно левую и правую safe area — они разные.
  • В Split View контролы левого приложения уходят влево, правого — вправо. Зеркальный случай тоже нужно нарисовать.
  • Layout margins тоже асимметричны: контент может подойти ближе к вертикальному бару, сохранив полное поле с противоположной стороны.
Разделяйте слои в макете
СлойПравило
Интерактив, текст, управлениевнутри safe area
Фон, артворк, hero-изображениеedge-to-edge, под safe area
Три схемы выравнивания на внешнем дисплее
  1. Автоофсет (default) — контент инсетнут по horizontal safe area, не прячется под контролами. Подходит почти всем.
  2. Центрирование по всему экрану, без офсета — только для иммерсивных визуальных нескроллящихся интерфейсов. Условие: интерактив не попадает под правый рейл.
  3. Гибрид — фон и хедер на всю ширину, скроллящийся контент инсетнут. Условие: весь интерактив живёт внутри скроллящейся области.
Зона внешней камеры
Зона внешней камеры присутствует всегда и разворачивается в Dynamic Island.
Calculator на iPhone 16 и на iPhone Duo
Схема №2 в действии. Слева iPhone 16 — 4 колонки по 5 кнопок. Справа внешний дисплей Duo — 5 колонок по 4. Макет не растянут, а пересобран под другую пропорцию.
⚙️ Dev
❌ Неправильно — предполагает симметрию
let width = view.bounds.width - view.safeAreaInsets.left * 2
✅ Правильно — каждая сторона отдельно
let width = view.bounds.inset(by: view.safeAreaInsets).width
// Foreground — в пределах safe area
foreground.frame = view.bounds.inset(by: view.safeAreaInsets)

// Background — edge-to-edge
backgroundView.frame = view.bounds            // UIKit
BackgroundView().ignoresSafeArea()            // SwiftUI
Свои кастомные бары — новый API iOS 27.1

Позволяет выйти за safe area, не конфликтуя с системным UI:

ReservedRegion          // SwiftUI
UIViewReservedRegion    // UIKit

✅ Готово, когда grep по кодовой базе не находит ни одного safeAreaInsets.left * 2 и подобных допущений, а макеты имеют оба зеркальных состояния для Split View.

🖼 Кадры из сессий Apple (4)
<b>Layout margin ≠ safe area inset.</b> Apple явно разделяет их в сессии Design for iPhone Duo.
Layout margin ≠ safe area inset. Apple явно разделяет их в сессии Design for iPhone Duo. ▶ 4:25
<b>То же на схеме.</b> Слева layout margin, справа safe area inset — величины разные и несимметричные.
То же на схеме. Слева layout margin, справа safe area inset — величины разные и несимметричные. ▶ 7:35
<b>Правильный расчёт.</b> <code>view.bounds.inset(by: view.safeAreaInsets)</code> вместо арифметики с одним инсетом.
Правильный расчёт. view.bounds.inset(by: view.safeAreaInsets) вместо арифметики с одним инсетом. ▶ 6:45
<b>Четыре правила safe area</b> из сессии Prepare your app.
Четыре правила safe area из сессии Prepare your app. ▶ 9:05

Часть 2 · Вертикальные бары

F3 Toolbar / Tab bar / Navigation на вертикальной оси

Суть. На внешнем дисплее и на внутреннем в landscape toolbar, tab bar, navigation controls, status bar и Dynamic Island переезжают в вертикальную полосу справа. Исключение: внутренний дисплей в portrait — там остаются привычные горизонтальные бары.

📎 Референсы — основное видео: Raise the bar with iPhone Duo
🎨 Design

Зачем: ширина есть, высоты мало. Контролы сбоку освобождают вертикаль под контент и попадают под большой палец правой руки.

Состав вертикальной полосы
Главная схема раздела. Выноски сверху вниз: Dynamic Island → status bar → toolbar → tab bar.
Что делит вертикальную полосу, сверху вниз
  1. Live Activities и Dynamic Island (система)
  2. Status bar (система)
  3. Ваши navigation-контролы — Back / Close
  4. Prominent action — Done
  5. Остальные toolbar-элементы в своих группах
  6. Tab bar — прижат к низу

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

Маппинг существующего приложения
БылоСтало
Top bar actionsверх вертикальной полосы
Bottom toolbar actionsниз вертикальной полосы
Tab barниз, под тулбаром
Что НЕ переезжает
  • Текстовые кнопки («Select», «Edit»)
  • Segmented controls
  • Всё, что слишком широко для фиксированной ширины полосы
  • Accessory bar над клавиатурой — остаётся прикреплённым к клавиатуре
Правила дизайна
  • Вертикальная полоса = symbol-only. Горизонтальный бар: фиксированная высота, гибкая ширина. Вертикальный: фиксированная ширина, гибкая высота. Поэтому туда идут только иконки.
  • Всегда задавайте и название, и символ для каждого нетекстового элемента. Название нужно системе для overflow-меню и развёрнутых форм, даже если вы его нигде не видите.
  • Минимизируйте текстовые кнопки. Тест: текст дублирует смысл символа → выбросьте текст. Текст несёт самостоятельную информацию (сумма в корзине) → оставьте элемент горизонтальным.
  • Паттерн «текст+иконка → иконка+бейдж»: вместо «Inbox 7» inline — иконка конверта с бейджем 7.
  • Группируйте, не расставляйте вручную. Никаких кастомных спейсеров — группы сами дают правильные отступы.
  • Держите позиции контролов постоянными между позами, чтобы человек не учил приложение заново при складывании.
  • RTL: полоса привязана к железу и остаётся на той же физической стороне. Зеркалится только контент.
  • Reduce Transparency: вертикальная полоса получает фон. Проверьте читаемость кастомного содержимого.
Mail: контролы левой и правой панели
Правило «контролы рядом со своим контентом». Mail: контролы левой панели остаются сверху неё, контролы правой — на вертикальной оси. Панели подсвечены цветом.
Где полоса появляется, а где нет
КонтекстПоведение
Split viewтолько detail-колонка; остальные — горизонтальные
Inspector (развёрнутый)не получает собственной полосы
Sheet на внешнем дисплеевертикальная полоса
Sheet на внутреннемцентрирован, горизонтальные бары
Sheet, смещённый вправополучает вертикальную полосу
Sheet, смещённый влевоне получает
Когда отключать — редко
  • Одностраничное приложение с «тяжёлым низом» — калькулятор.
  • Sheet с единственной кнопкой Close — полоса съедает пространство зря. Тогда sheet останавливается чуть ниже фронтальной камеры, а status bar перепозиционируется.
HIG прямо говорит: в общем случае не переопределяйте дефолтное размещение. Позиция контролов на вертикальной оси — core-паттерн платформы.
⚙️ Dev
Включение — два шага, оба обязательны

1. Пересобрать против iOS 27.1 SDK.   2. Использовать бары от навигационных контейнеров:

// SwiftUI ✅
NavigationStack {
    ContentView()
        .toolbar {
            ToolbarItem(placement: .bottomBar) { … }
        }
}

// UIKit ✅
let vc = UIViewController()
vc.toolbarItems = [...]
let nav = UINavigationController(rootViewController: vc)

// UIKit ❌ — содержимое будет проигнорировано, ошибки не будет
let toolbar = UIToolbar()
toolbar.items = [...]
⚠️ Кастомно созданные UIToolbar, UINavigationBar, UITabBar молча не получат вертикальную раскладку.
Размещение элементов
// Back / Close
.toolbar { ToolbarItem(placement: .cancellationAction) { … } }
// UIKit:
navigationItem.leftItemsSupplementBackButton = false   // default
navigationItem.leadingItemGroups = [UIBarButtonItemGroup(...)]

// Prominent action (Done)
.toolbar { ToolbarItem(placement: .topBarPinnedTrailing) { … } }
// UIKit:
navigationItem.pinnedTrailingGroup = UIBarButtonItemGroup(...)
Управление осью вручную
// Элемент, переключающийся текст ↔ символ (кастомный Select/Done)
.axisBehavior(.horizontalOnly)
item.axisBehavior = .horizontalOnly

// Кастомный view, умеющий вертикальное представление (напр. компас)
.axisBehavior(.verticalPreferred)
let item = UIBarButtonItem(customView: CompassView())
item.axisBehavior = .verticalPreferred

Системная Edit-кнопка уже ведёт себя правильно автоматически.

Бейджи (iOS 26+)
InboxButton().badge(7)        // SwiftUI
item.badge = .count(7)        // UIKit
Чтение контекста в кастомных view
@Environment(\.toolbarVerticalEdge) var edge   // SwiftUI
traitCollection.verticalBarEdge                // UIKit
// nil / .unspecified — когда вертикального бара нет

Кастомные view, идущие вертикально, должны либо влезать в фиксированную ширину, либо иметь вертикально адаптированную раскладку — например прятать подписи и становиться ниже.

Спейсеры: flexible в вертикальной оси = нулевой размер; fixed уважают минимум. Используйте ToolbarItemGroup / UIBarButtonItemGroup.

Отключение
.toolbarVerticalBehavior(.disabled)   // SwiftUI

override var preferredVerticalBarBehavior: UIVerticalBarBehavior { .disabled }  // UIKit

✅ Готово, когда нет ни одного ручного UIToolbar, каждый нетекстовый item имеет title и symbol, RTL и Reduce Transparency проверены.

🖼 Кадры из сессий Apple (9)
<b>Что живёт в вертикальной полосе.</b> Live Activities · status bar · app controls — сверху вниз.
Что живёт в вертикальной полосе. Live Activities · status bar · app controls — сверху вниз. ▶ 5:45
<b>Back / Close.</b> <code>ToolbarItem(placement: .cancellationAction)</code> и <code>navigationItem.leadingItemGroups</code>.
Back / Close. ToolbarItem(placement: .cancellationAction) и navigationItem.leadingItemGroups. ▶ 5:05
<b>Закреплённая группа.</b> <code>.topBarPinnedTrailing</code> / <code>pinnedTrailingGroup</code>.
Закреплённая группа. .topBarPinnedTrailing / pinnedTrailingGroup. ▶ 5:25
<b>Ловушка №1.</b> ✅ <code>toolbarItems</code> / <code>navigationItem</code> получают вертикальную раскладку. ❌ Свой <code>UIToolbar</code> — нет, и <em>молча</em>.
Ловушка №1.toolbarItems / navigationItem получают вертикальную раскладку. ❌ Свой UIToolbar — нет, и молча. ▶ 2:45
<b>Узнать сторону бара:</b> <code>@Environment(\.toolbarVerticalEdge)</code> / <code>traitCollection.verticalBarEdge</code>.
Узнать сторону бара: @Environment(\.toolbarVerticalEdge) / traitCollection.verticalBarEdge. ▶ 10:45
<b><code>.axisBehavior(.horizontalOnly)</code></b> — элемент, который не должен уезжать в вертикальный бар.
.axisBehavior(.horizontalOnly) — элемент, который не должен уезжать в вертикальный бар. ▶ 8:35
<b><code>.axisBehavior(.verticalPreferred)</code></b> — наоборот, элемент, которому в вертикальном баре лучше.
.axisBehavior(.verticalPreferred) — наоборот, элемент, которому в вертикальном баре лучше. ▶ 8:55
<b>Всегда задавайте title.</b> В вертикальном баре подпись и accessibility-имя нужны обязательно.
Всегда задавайте title. В вертикальном баре подпись и accessibility-имя нужны обязательно. ▶ 7:25
<b>RTL.</b> Вертикальная полоса зеркалится вместе с интерфейсом.
RTL. Вертикальная полоса зеркалится вместе с интерфейсом. ▶ 4:25

F4 Overflow и компрессия баров

Суть. На внешнем дисплее в landscape вертикали мало, ещё и клавиатура с PiP конкурируют за место. Переполнение — норма, а не край.

📎 Референсы
🎨 Design
Решение №1 — чем жертвуем первым, toolbar или tab bar?

Это продуктовое решение, принимается для каждого экрана отдельно.

Тип экранаЧто сжимается первымЛогика
Navigation-focused
лента, каталог
toolbar → в overflowглавные разделы должны оставаться доступными. Это дефолт.
Task-oriented
экран выполнения задачи
tab bar минимизируетсядействия для задачи важнее переключения разделов
Сжат toolbar, tab bar цел
Вариант A — сжат toolbar, tab bar цел. Дефолт, navigation-focused. Тулбар уехал в overflow, разделы остались доступны.
Сжат tab bar, toolbar цел
Вариант B — сжат tab bar, toolbar цел. Task-oriented. Таббар свёрнут в один контрол, действия сохранены.
Решение №2 — приоритеты видимости

По умолчанию элементы уходят в overflow снизу вверх. Нужно явно сказать системе, что важнее:

  • Дольше всего остаются частые действия: Compose в почте, New Note в заметках.
  • Также долго остаются элементы со статусом — всё, что имеет бейдж, потому что их смысл в glanceability.
  • Приоритизируйте сначала группами, потом отдельными элементами внутри группы.
Решение №3 — одно overflow-меню
  • Собственное «…» меню нужно влить в системное, чтобы человек искал всё в одном месте.
  • Эллипсис зарезервирован исключительно под overflow.
  • Другим меню дайте собственный, содержательный символ.
  • Не тащите символы с других платформ (kebab, hamburger).
В макетах показать три состояния: полный бар · частичный overflow · максимальное сжатие (landscape + клавиатура).
⚙️ Dev
// Приоритет toolbar vs tab bar — на уровне экрана
TabView {
    Tab("Recents", systemImage: "clock") {
        ContentView()
            .toolbarVerticalCompressionBehavior(.prefersToolbarItems)
    }
}
// UIKit:
navigationItem.verticalBarCompressionBehavior = .prefersBarItems
// Слияние собственного overflow с системным
.toolbar {
    ToolbarOverflowMenu {
        Button("Scan")    { … }
        Button("Connect") { … }
    }
}
// UIKit:
navigationItem.additionalOverflowItems = UIDeferredMenuElement { provider in
    provider(self.persistentOverflowItems())
}
// Приоритеты видимости
.toolbar {
    ToolbarItem { Button(…) { … } }
        .visibilityPriority(.high)      // .high / .low / кастомное
}
// UIKit:
item.visibilityPriority = .high

✅ Готово, когда для каждого экрана осознанно выбран compression behavior, у всех toolbar items выставлен приоритет, собственного overflow-меню в приложении больше не существует.

🖼 Кадры из сессий Apple (5)
<b>Компрессия.</b> <code>toolbarVerticalCompressionBehavior</code> / <code>verticalBarCompressionBehavior</code> — что сжимать первым.
Компрессия. toolbarVerticalCompressionBehavior / verticalBarCompressionBehavior — что сжимать первым. ▶ 12:25
<b>Overflow.</b> <code>ToolbarOverflowMenu</code> (SwiftUI) и <code>additionalOverflowItems</code> (UIKit).
Overflow. ToolbarOverflowMenu (SwiftUI) и additionalOverflowItems (UIKit). ▶ 12:45
<b>Как это выглядит.</b> Лишние элементы уходят под «…» внизу вертикальной полосы.
Как это выглядит. Лишние элементы уходят под «…» внизу вертикальной полосы. ▶ 13:25
<b>Приоритеты.</b> <code>.visibilityPriority(.high / .low / custom)</code> решает, кто останется на виду.
Приоритеты. .visibilityPriority(.high / .low / custom) решает, кто останется на виду. ▶ 13:45
<b>Calculator.</b> Макет не растянут, а пересобран под другую пропорцию.
Calculator. Макет не растянут, а пересобран под другую пропорцию. ▶ 14:35

Часть 3 · Сгиб и адаптивные контейнеры

F5 Reserved regions и displacement

Суть. На Duo пространство формируют не только safe areas, но и reserved regions — зоны, которые контент должен обходить.

📎 Референсы — основное видео: Strike a pose with adaptive layouts
  • Strike a pose (YouTube · Tech Talk 111463): 00:42 что такое reserved regions · 01:42 аналогия с книжным корешком · 02:26 принцип displacement · 03:46 что НЕ смещается · 04:00 куда двигать по позам · 05:40 split view 50/50 · 05:51 пример с сеткой · 06:38 API
  • Design for iPhone Duo: 02:03 поведение контента у сгиба · 08:37 fold-avoidance в системных компонентах
  • HIG: Reserved regions
🎨 Design
Три reserved regions
РегионТипКогда активен
Внешняя фронт-камераocclusionвсегда, разворачивается в Dynamic Island
Внутренняя фронт-камераocclusionтолько когда камера активна — UI разъезжается, обозначая её присутствие
Сгиб (fold)divisionтолько когда устройство частично сложено — делит дисплей на две рабочие зоны

Ментальная модель: это то же, к чему ваш макет уже адаптируется на iPad (window controls).

Внутренний дисплей: зона сгиба и камеры
Внутренний дисплей — зона сгиба (division) и зона внутренней камеры (occlusion)
Внешний дисплей: зона камеры
Внешний дисплей — зона камеры, активна всегда
Аналогия Apple. Фотография, напечатанная на развороте книги через корешок. У корешка часть изображения становится нечитаемой, и оно перестаёт восприниматься как цельное. С UI происходит то же самое.
Displacement — паттерн смещения

Цель формулируется тремя словами: контент должен оставаться visible, reachable, unobstructed.

Не смещаетсяСмещается
Непрерывный скролл-контент: статьи, ленты, документы, списки, фотосетки. Они уже адаптируются прокруткой — смещение ломает непрерывность.Алерты, меню, попапы, action sheets, отдельные контролы, иногда целые контейнеры.
Правила смещения
  • Выбирайте scope. Элемент, адаптирующийся самостоятельно — двигайте отдельно. Связанные элементы — вместе. Пример: контекстное меню фотографии двигается вместе с самой фотографией и выравнивается у сгиба, а не улетает отдельно в правую половину.
  • Избегайте избыточного движения. Далёкое смещение разрушает визуальную связь между элементом и его источником.
  • Избегайте резких изменений. Контрол, который исчезает или прыгает далеко, сложнее найти. Мелкая корректировка лучше перестановки.
Куда двигать — зависит от позы
ПозаЛогика размещения
«Книжка»алерты уходят на trailing-сторону — туда, где они продолжат жизнь, когда устройство закроют
Tabletopверх — контент, который смотрят издалека. Низ — тапабельные контролы: стабильная поверхность для касания
Любаяприоритет — контекстность: поле поиска остаётся над тем view, который ищет, меняя ширину и позицию
  • Адаптируется не только позиция и размер, но и margins, corner radius.
  • Сетки: если на экране может появиться сгиб — предпочитайте чётное количество колонок. Решение принимается один раз, независимо от того, активен ли сгиб сейчас.
  • Grid-пример: сохраняем внешние поля и увеличиваем отступ вокруг сгиба, чтобы каждая карточка осталась внутри своей зоны и тапабельной.
  • Split view: система сама подгоняет ширины колонок к ровному 50/50 вокруг сгиба.
Notes до и после складывания — эталонный пример
Notes на полностью открытом дисплее
Полностью открыт — левая панель уже правой
Notes на частично сложенном дисплее
Частично сложен — панели выровнялись в 50/50, каждая в своей зоне
Главный аргумент за системные компоненты. Sheets, alerts, menus, popovers, toolbar buttons, split views уже имеют fold-avoidance встроенно. Кастом — не имеет.
⚙️ Dev
// SwiftUI — через GeometryReader или onGeometryChange
GeometryReader { proxy in
    let regions = proxy.reservedRegions(kind: .division)
    let frames  = regions.map(\.frame)
}

// Включая неактивные
proxy.reservedRegions(kind: .division, options: .includeInactive)

// Камеры
proxy.reservedRegions(kind: .occlusion)

// UIKit
let regions = view.reservedRegions(kind: .division)
  • Division region делит площадь на несколько зон. Активен только при сложенном устройстве; когда плоское — неактивен, ширина 0.
  • Occlusion region не делит, а перекрывает — маленький фрейм внутри ваших bounds.
  • Зачем includeInactive: позволяет принимать решения высокого уровня заранее. Например, выбрать чётное количество колонок в сетке по одному факту наличия division region, не дожидаясь складывания.

✅ Готово, когда все центрированные макеты проаудированы, высокоприоритетные кастомные контролы используют reserved regions API, сетки имеют чётное количество колонок там, где это уместно.

🖼 Кадры из сессий Apple (7)
<b>API reserved regions.</b> <code>proxy.reservedRegions(kind: .division)</code> / <code>view.reservedRegions(kind:)</code>.
API reserved regions. proxy.reservedRegions(kind: .division) / view.reservedRegions(kind:). ▶ 6:55
<b><code>options: .includeInactive</code></b> — чтобы получить регион даже когда он сейчас неактивен (например, камера выключена).
options: .includeInactive — чтобы получить регион даже когда он сейчас неактивен (например, камера выключена). ▶ 7:25
<b>Occlusion.</b> Внутренняя under-display камера перекрывает контент только когда активна.
Occlusion. Внутренняя under-display камера перекрывает контент только когда активна. ▶ 8:15
<b>Displacement в действии.</b> Меню смещается, чтобы не попасть на сгиб.
Displacement в действии. Меню смещается, чтобы не попасть на сгиб. ▶ 3:35
<b>То же меню на сложенном устройстве.</b> Система сама выбирает сторону.
То же меню на сложенном устройстве. Система сама выбирает сторону. ▶ 3:45
<b>Popover.</b> Система разводит контент по половинам относительно сгиба.
Popover. Система разводит контент по половинам относительно сгиба. ▶ 5:25
<b>Для своих кастомных баров.</b> <code>ReservedRegion</code> / <code>UIViewReservedRegion</code> — новое в iOS 27.1.
Для своих кастомных баров. ReservedRegion / UIViewReservedRegion — новое в iOS 27.1. ▶ 8:25

F6 ArrangementView — новый layout-контейнер

Суть. Контейнер, который держит primary и secondary view и сам решает, как их разложить, учитывая size class, соотношение сторон и активные reserved regions.

📎 Референсы
  • Strike a pose: 08:21 место layout-контейнеров в иерархии · 09:33 разбор на примере Podcasts · 10:31 входы и выходы · 11:20 код · 12:34 ограничение оси · 13:16 overlay · 14:32 как выбрать стиль · 16:08 когда НЕ использовать
  • HIG: Arrangement views
  • API: HStack · VStack · ZStack — исходные паттерны для переноса
🎨 Design
Место в иерархии контейнеров
Навигационные:  NavigationStack · NavigationSplitView · TabView
Layout:         ArrangementView            ← новый уровень
Контентные:     List · ScrollView
Что это даёт — пример с плеером и транскриптом
  • iPad, широко → плеер и транскрипт рядом
  • Duo открыт → то же самое
  • Duo сложен, транскрипт выключен → плеер не центрируется по всему экрану, а остаётся в своей левой зоне, чтобы контролы были достижимы и не попадали на сгиб
  • Duo повёрнут вертикально → split не применяется вовсе, транскрипт показывается inline
Два стиля
СтильПоведениеКогда выбирать
Split
дефолт
делит площадь между двумя view. Горизонтально — если шире высоты; вертикально — если выше шириныmain / detail связь. Ни один из двух view нельзя перекрывать. Пример: плеер + транскрипт
Overlayнакладывает один на другой. При складывании — расходятся по половинамforeground / background связь. Частичное перекрытие приемлемо, фон можно проскроллить. Пример: контролы читалки поверх текста
Split arrangement
Split — площадь делится, ничто не перекрывается
Overlay arrangement
Overlay — primary наложен поверх secondary
Как выбрать — правило переноса
У вас сейчасБерите
HStack / VStack-подобный макетSplit
ZStack-подобный макетOverlay
Ничего похожего нетпо типу связи: main/detail → split, foreground/background → overlay
  • Можно ограничить ось: «делиться только горизонтально». Если split не может разделиться вдоль основной оси — покажется только один view. Это нужно нарисовать.
  • В overlay вторичный view может иметь свёрнутое и развёрнутое состояние — при складывании он получает больше места и может раскрыться. Оба состояния должны быть в макете.
  • Это не замена NavigationSplitView. Нужна навигация со сворачиванием колонок — берите split view.
⚙️ Dev
// SwiftUI
NavigationStack {
    ArrangementView {
        PlayerView()
    } secondary: {
        UpNextView()
    }
    .arrangementViewStyle(.split.axes(.horizontal))   // или .overlay
}
// UIKit
let arrangementVC = UIArrangementViewController()
let nav = UINavigationController(rootViewController: arrangementVC)

arrangementVC.setViewController(playerVC, for: .primary)
arrangementVC.setViewController(upNextVC, for: .secondary)
arrangementVC.updateArrangement(.split.axes(.horizontal))
Overlay: читаем z-index, чтобы изменить представление
struct UpNextView: View {
    @Environment(\.overlayArrangementZIndex) private var zIndex: Int
    var minimization: UpNextMinimization { zIndex > 0 ? .collapsed : .expanded }
    var body: some View { UpNextList(minimization: minimization) }
}

// UIKit
let primaryState = arrangementVC.state(for: .primary)
model.minimization = (primaryState?.zIndex ?? 0) > 0 ? .collapsed : .expanded
Ограничения
  • Не кладите навигационные контейнеры (NavigationSplitView, TabView) внутрь ArrangementView — он не даёт навигационной инфраструктуры. Ставьте их вокруг.
  • Не кладите ArrangementView внутрь List или ScrollView.

✅ Готово, когда кастомные двухколоночные и наложенные макеты переведены на ArrangementView, навигация осталась снаружи, оба состояния secondary view нарисованы и реализованы.

🖼 Кадры из сессий Apple (10)
<b>Таксономия контейнеров.</b> Navigation · Layout · Content. <code>ArrangementView</code> — новый layout-контейнер.
Таксономия контейнеров. Navigation · Layout · Content. ArrangementView — новый layout-контейнер. ▶ 9:25
<b>Как работает Arrangement.</b> Входы: size class, aspect ratio, reserved region → выходы: visibility и frames.
Как работает Arrangement. Входы: size class, aspect ratio, reserved region → выходы: visibility и frames. ▶ 11:05
<b><code>.arrangementViewStyle(.split)</code></b> — две панели рядом.
.arrangementViewStyle(.split) — две панели рядом. ▶ 12:15
<b>Та же <code>.split</code> в другой позе</b> — система сама переключает ось.
Та же .split в другой позе — система сама переключает ось. ▶ 12:35
<b><code>.split.axes(.horizontal)</code></b> — ограничить ось; при нехватке места вторая панель схлопывается.
.split.axes(.horizontal) — ограничить ось; при нехватке места вторая панель схлопывается. ▶ 12:45
<b><code>.arrangementViewStyle(.overlay)</code></b> — вторичный контент поверх основного.
.arrangementViewStyle(.overlay) — вторичный контент поверх основного. ▶ 13:45
<b><code>overlayArrangementZIndex</code></b> — узнать, находится ли ваша вью сверху, и подстроить её плотность.
overlayArrangementZIndex — узнать, находится ли ваша вью сверху, и подстроить её плотность. ▶ 14:05
<b>Когда overlay.</b> Notes: фон — контент, поверх — плеер.
Когда overlay. Notes: фон — контент, поверх — плеер. ▶ 15:25
<b>Когда split.</b> Podcasts: плеер и транскрипт равнозначны.
Когда split. Podcasts: плеер и транскрипт равнозначны. ▶ 15:45
<b>Правило выбора.</b> Следуйте существующим паттернам своего приложения.
Правило выбора. Следуйте существующим паттернам своего приложения. ▶ 14:45

Часть 4 · Навигация и презентации

F7 Внутренний дисплей — три стратегии

Суть. Нельзя просто растянуть iPhone-приложение на большой экран.

📎 Референсы
🎨 Design
СтратегияЧто делаетКому подходитПример
1. Split viewпоказывает несколько уровней иерархии одновременноприложения со списком и детальюMail
2. Reflow в колонкивертикальный стек перестраивается в 2 колонкиконтентные экраны со сложенным хедеромMusic
3. Tab bar → Sidebarнижний таббар становится левым сайдбаромтолько info-dense приложенияHealth
Mail — эталон стратегии №1
Mail на внешнем дисплее
Внешний — один уровень иерархии (письмо)
Mail на внутреннем дисплее
Внутренний — та же иерархия, но оба уровня сразу
Ограничения, общие для всех трёх
  • Иерархия не меняется между внешним и внутренним дисплеем. Split view просто показывает оба уровня сразу вместо одного.
  • Ни одна функция не привязана к одному дисплею.
  • Sidebar — не для всех. Если у вас 3–4 простых таба, сайдбар лишний.
  • Split view на внешнем дисплее сворачивается в одну колонку — так же, как между regular и compact на любом iPhone.
⚙️ Dev
// Split
NavigationSplitView { … } detail: { … }     // SwiftUI
UISplitViewController                        // UIKit

// Tab bar как sidebar
TabView { … }.defaultTabBarPlacement(.sidebar)          // SwiftUI
tabBarController.sidebar.preferredPlacement = .sidebar  // UIKit

Split view, собранный из стандартных компонентов, сам адаптирует ширины колонок и поля под сгиб.

✅ Готово, когда для внутреннего дисплея осознанно выбрана одна из стратегий, а иерархия идентична на обоих дисплеях.

🖼 Кадры из сессий Apple (4)
<b>Стратегия 1 — <code>NavigationSplitView</code>.</b> Список + деталь.
Стратегия 1 — NavigationSplitView. Список + деталь. ▶ 5:25
<b>Стратегия 2 — <code>TabView</code></b> с боковой раскладкой.
Стратегия 2 — TabView с боковой раскладкой. ▶ 5:35
<b>Стратегия 3 — <code>defaultTabBarPlacement(.sidebar)</code>.</b>
Стратегия 3 — defaultTabBarPlacement(.sidebar). ▶ 5:45
<b>Непрерывность.</b> При складывании/раскладывании пользователь должен остаться на том же контенте.
Непрерывность. При складывании/раскладывании пользователь должен остаться на том же контенте. ▶ 1:55

F8 Sheets, alerts, menus, popovers

Суть. Все презентации адаптируются к позе автоматически — если они системные.

📎 Референсы
🎨 Design
КонтекстПоведение sheet
Внешний дисплейконтролы переезжают в вертикальную полосу; кнопки могут раскладываться вертикально
Внешний, sheet с одной кнопкойможно отключить вертикальную полосу → sheet останавливается чуть ниже фронт-камеры, status bar перепозиционируется
Внутренний, portrait и landscapeцентрирован, стандартные горизонтальные бары
Частично сложенсъезжает вбок со сгиба

Fold-avoidance встроен в: sheets, alerts, menus, action sheets, popovers, toolbar buttons и другие системные компоненты. Кнопки, попадающие на сгиб, трудно нажать — поэтому система их отодвигает.

Правило для дизайна. Проектируйте диалоги и меню системными компонентами. Кастомный модальный слой придётся руками смещать через reserved regions API — и делать это на уровне каждой позы.
⚙️ Dev
  • Системные .sheet, .alert, .confirmationDialog, .popover, UIMenu — из коробки.
  • Для кастомных оверлеев — reservedRegions(kind: .division) и смещение вручную.
  • Управление наличием бара в sheet — через toolbarVerticalBehavior(.disabled) / preferredVerticalBarBehavior.

✅ Готово, когда нет ни одного кастомного модального слоя без обработки division region.

🖼 Кадры из сессий Apple (1)
<b>Sheets на внутреннем дисплее.</b> Система сама выбирает размер и позицию относительно сгиба.
Sheets на внутреннем дисплее. Система сама выбирает размер и позицию относительно сгиба. ▶ 6:05

Часть 5 · Позы и уникальные возможности

F9 Опциональный tabletop-layout

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

📎 Референсы
  • 04:36Design for iPhone Duoпример Apple Music: лирика сверху, транспорт снизу + условие функциональной эквивалентности
  • 04:23Strike a pose — логика «верх смотреть, низ трогать»
  • HIG: Device poses
🎨 Design

Устройство стоит на столе, руки свободны. Естественное распределение:

  • Верхняя половина — медиа и контент, который смотрят (обложка, видео, лирика)
  • Нижняя половина — тапабельные контролы на стабильной поверхности (transport controls, скраббер, клавиатура)
Жёсткое условие. Этот макет должен содержать те же контролы и ту же иерархию, что и остальные позы. Нельзя давать функцию только в tabletop.

Это опция, а не требование. Если приложение не медийное — пропускайте.

⚙️ Dev

Реализуется через reserved regions (.division) или overlay-arrangement, а не через угол шарнира.

✅ Готово, когда либо макет не делался вообще, либо он функционально эквивалентен другим позам.

🖼 Кадры из сессий Apple (3)
<b>Tabletop.</b> Нижняя половина — ввод, верхняя — контент.
Tabletop. Нижняя половина — ввод, верхняя — контент. ▶ 0:55
<b>FaceTime.</b> Классический сценарий hands-free.
FaceTime. Классический сценарий hands-free. ▶ 2:15
<b>Apple TV.</b> Видео сверху, транспорт снизу.
Apple TV. Видео сверху, транспорт снизу. ▶ 2:35

F10 Split View multitasking и PiP

Суть. Все приложения участвуют, отказаться нельзя. Контролы уходят к внешнему краю.

📎 Референсы
  • 02:43Design for iPhone Duoжест 50/50, контролы к внешнему краю, PiP с прикреплением к верху
  • Leverage multiple displays: 02:59 две раскладки · 03:25 инструменты
  • 07:51Prepare your appкак тестировать в симуляторе
🎨 Design
Два приложения в Split View
Контролы уходят к внешним краям. Левое приложение — контролы слева, правое — справа. Центр остаётся свободным.
  • Две раскладки: 50/50 рядом и стопкой (видео сверху + приложение снизу). Для вас они ведут себя одинаково.
  • Жест: перетащить приложение от home-индикатора к краю.
  • PiP: видео можно прикрепить к верху экрана. Приложение ресайзится вертикально в реальном времени. При складывании видео растягивается на половину экрана — приложение снова подстраивается.
  • В макетах нужны зеркальное состояние (контролы слева) и состояние уменьшенной высоты (PiP сверху).
⚙️ Dev
  • Инструменты те же: size classes + scene geometry. Ничего специального.
  • Если работает resizing на iPad или в iPhone Mirroring — базово уже работает.
  • Тест в симуляторе: открыть на внутреннем дисплее в Device Hub, потянуть от home-индикатора вбок. Проследить, как вертикальные бары перепрыгивают на противоположную сторону.

✅ Готово, когда приложение корректно в левой и правой половине и при сжатии по высоте через PiP.

🖼 Кадры из сессий Apple (3)
<b>Split View.</b> Два приложения одновременно на внутреннем дисплее.
Split View. Два приложения одновременно на внутреннем дисплее. ▶ 2:55
<b>Ваше приложение может оказаться в половине экрана</b> в любой момент.
Ваше приложение может оказаться в половине экрана в любой момент. ▶ 3:05
<b>Параллельная работа.</b> Ваш UI должен выживать в узкой колонке.
Параллельная работа. Ваш UI должен выживать в узкой колонке. ▶ 3:45

F11 Несколько сцен (multiple windows)

Суть. Duo — первый iPhone с несколькими одновременными инстансами UI приложения. Но с важным ограничением.

📎 Референсы
  • Leverage multiple displays: 03:38 несколько инстансов UI · 03:48 ограничение внешнего дисплея · 04:01 обработка недоступности
🎨 Design
⚠️ На внешнем дисплее новые окна создать нельзя. Это доступно только на внутреннем.

Значит кнопка «открыть в новом окне» должна исчезать, а не быть неактивной или давать ошибку.

⚙️ Dev
  • Если поддерживаете multiple scenes на iPad — работает и здесь.
  • Доступность динамическая — в отличие от iPad, где окно можно создать в любой момент.
  • Обрабатывайте ошибки создания сцены.
  • Используйте UIWindowScene.ActivationAction — он автоматически прячется, когда создание недоступно.

✅ Готово, когда нет ни одного пути, где запрос новой сцены на внешнем дисплее даёт ошибку пользователю.

🖼 Кадры из сессий Apple (2)
<b><code>UIWindowScene.ActivationAction</code></b> — корректный способ открыть новую сцену.
UIWindowScene.ActivationAction — корректный способ открыть новую сцену. ▶ 4:25
<b>Обрабатывайте отказ.</b> Доступность новых сцен теперь динамическая — запрос может не пройти.
Обрабатывайте отказ. Доступность новых сцен теперь динамическая — запрос может не пройти. ▶ 3:55

F12 Hinge API — интеракции и эффекты

Суть. Угол шарнира доступен непрерывно, не только как «открыто/закрыто».

📎 Референсы
  • Leverage multiple displays: 00:48 реакция обоев на угол · 01:31 разбор кода · 02:22 демо гитары с вибрато · 02:35 граница «эффекты vs layout»
🎨 Design
  • Системный пример: обои плавно зумятся во время раскрытия.
  • Пример из сессии: гитарное приложение, где сгиб работает как рычаг вибрато и плавно гнёт высоту звука.
  • Эффект должен быть дополнением, а не единственным способом что-то сделать — правило «функционал не привязан к позе».
Граница ответственности. Шарнир — для интеракций, эффектов, выразительности. Для layout — reserved regions и arrangements. Не стройте раскладку на угле.
⚙️ Dev
struct InstrumentView: View {
    @State private var pitchBend: Double = 0

    var body: some View {
        GuitarView(pitchBend: pitchBend)
            .onHingeChange { _, context in
                if let hinge = context.hinge, hinge.status == .partiallyOpen {
                    pitchBend = calculatePitchBend(angle: hinge.angle)
                } else {
                    pitchBend = 0     // обязательный сброс
                }
            }
    }
}
  • UIKit: UIHingeInteraction.
  • Даёт дискретный status: .closed / .partiallyOpen / .fullyOpen, и непрерывный angle.
  • context.hinge == nil → устройство без шарнира. Обрабатывать обязательно.
  • Не забывайте else-ветку со сбросом состояния.

✅ Готово, когда эффект корректно деградирует на устройствах без шарнира, а layout от угла не зависит.

🖼 Кадры из сессий Apple (3)
<b>Hinge API.</b> <code>.onHingeChange</code> (SwiftUI) и <code>UIHingeInteraction</code> (UIKit): дискретный статус + непрерывный угол.
Hinge API. .onHingeChange (SwiftUI) и UIHingeInteraction (UIKit): дискретный статус + непрерывный угол. ▶ 1:15
<b>Пример.</b> Угол сгиба управляет pitch bend в приложении-гитаре.
Пример. Угол сгиба управляет pitch bend в приложении-гитаре. ▶ 2:15
<b>Граница ответственности.</b> Hinge — для интеракций и эффектов; для layout — arrangements и regions.
Граница ответственности. Hinge — для интеракций и эффектов; для layout — arrangements и regions. ▶ 2:45

F13 Scene accessories — контент на двух экранах

Суть. Дополнительный контент на втором дисплее параллельно с основным UI.

📎 Референсы
  • Leverage multiple displays: 04:26 что такое scene accessories · 05:00 CameraCaptureAccessory и его условия · 05:43 демо телесуфлёра · 06:10 переключатель и доступность
  • 05:27Build a great camera experience — связка с камерой
🎨 Design
  • Общий кейс (iPhone/iPad): телефон как геймпад, игра на внешнем экране.
  • Новый кейс для Duo — CameraCaptureAccessory: основное UI камеры на внутреннем дисплее, а на внешнем — что-то для человека, которого снимают:
    • Телесуфлёр для того, кто записывает видео
    • Мультик или анимация, чтобы ребёнок смотрел в камеру
    • Превью для модели во время съёмки
⚠️ Система динамически включает и выключает доступность. UI должен реагировать: переключатель прячется или становится неактивным, когда аксессуар недоступен — например устройство закрыли. Это бонусная фича, не замена основного флоу.
⚙️ Dev
struct CameraRootView: View {
    @State private var model = TeleprompterModel()

    var body: some View {
        CameraView(model: model)
            .sceneAccessory {
                CameraCaptureAccessory(isEnabled: $model.isEnabled) {
                    TeleprompterView(model: model)
                }
                .onAvailabilityChange { model.isAvailable = $0 }
            }
            .toolbar {
                TeleprompterToggle(isEnabled: $model.isEnabled)
                    .disabled(!model.isAvailable)
            }
    }
}
Условия доступности CameraCaptureAccessory
  • приложение fullscreen на внутреннем дисплее
  • активная camera session
  • регистрировать на том же view, что и камерный UI — тогда аксессуар живёт ровно столько, сколько видна камера

✅ Готово, когда UI переключателя корректно реагирует на все изменения доступности, включая закрытие устройства во время записи.

🖼 Кадры из сессий Apple (4)
<b>Scene accessories.</b> Дополнительный контент рядом с основным UI; доступность контролирует система.
Scene accessories. Дополнительный контент рядом с основным UI; доступность контролирует система. ▶ 4:55
<b>Условия для <code>CameraCaptureAccessory</code>:</b> fullscreen на внутреннем дисплее, активная camera session, регистрация на той же вью.
Условия для CameraCaptureAccessory: fullscreen на внутреннем дисплее, активная camera session, регистрация на той же вью. ▶ 5:25
<b>Пример.</b> Телесуфлёр на внешнем дисплее, пока камера снимает с внутреннего.
Пример. Телесуфлёр на внешнем дисплее, пока камера снимает с внутреннего. ▶ 6:05
<b>Код целиком</b> — включая <code>onAvailabilityChange</code> и связанный toggle в toolbar.
Код целиком — включая onAvailabilityChange и связанный toggle в toolbar. ▶ 6:25

F14 Камера

Суть. Две фронтальные камеры с квадратным сенсором. «Фронтальная» больше не означает «смотрит на пользователя».

📎 Референсы — основное видео: Build a great camera experience
  • Build a great camera experience (YouTube): 00:30 две фронтальные камеры · 01:14 виртуальная фронтальная · 02:10 таблица возможностей и цена простоты · 03:14 почему .front больше не значит «смотрит на вас» · 04:02 direction coordinator · 05:38 concurrency · 06:34 зеркалирование · 07:11 компоновка превью · 08:38 оптимизация
  • Дока: AVFoundation — «Choosing a camera by the direction it faces», «Supporting device rotation in your camera app»
🎨 Design
КамераРазрешениеFPSDepth
Внутренняя (under-display)1080p60
Внешняя4K120
«Виртуальная фронтальная»
авто-переключение
1080p60
Продуктовое решение №1 — простота или возможности

Брать простую «виртуальную фронтальную» камеру (сама переключается при открытии и закрытии) или управлять обеими вручную? Цена простоты — общий знаменатель возможностей и отсутствие depth: нет портретного режима и эффектов глубины.

Продуктовое решение №2 — зеркалирование

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

Превью на внутреннем дисплее

При полном поле зрения задней камеры вокруг превью остаётся свободное пространство. Два варианта, выбирает дизайн:

  1. Сдвинуть превью и сгруппировать контролы в свободной зоне
  2. Растянуть превью на весь дисплей

Квадратный сенсор фронталок позволяет заполнить экран landscape-кадром на внутреннем дисплее. Внутренняя камера — occlusion region: когда неактивна, её не видно; при активации UI разъезжается. Если видоискатель центральный для вашего опыта — держите важный контент и контролы подальше от этой зоны.

⚙️ Dev
// Типы устройств
.builtInOuterUltrawideCamera      // новый
.builtInInnerUltrawideCamera      // новый
// Виртуальная: обычный DiscoverySession с position: .front + wide/ultrawide
// Direction coordinator — где камеры относительно КОНКРЕТНОГО view
directionCoordinator = AVCaptureDeviceDirectionCoordinator(
    view: view,
    deviceTypes: [.builtInOuterUltrawideCamera,
                  .builtInInnerUltrawideCamera,
                  .builtInDualWideCamera],
    changeHandler: { [weak self] map in self?.updateCameraSession(map) }
)
Правила concurrency
  • Coordinator изолирован к Main Actor.
  • В change handler не вызывайте AVFoundation напрямую. Он отдаёт AVCaptureDeviceDescriptor — Sendable-представление, которое безопасно передаётся в ваш camera actor, и уже там создаётся AVCaptureDevice.
  • Используете оба дисплея одновременно — отдельный coordinator на каждый view; каждый отчитывается относительно своего view.

В change handler три вещи: переконфигурировать AVCaptureSession · пересмотреть решение о зеркалировании · обновить UI.

Превью и производительность
previewLayer.videoGravity            // как превью ложится в bounds слоя
device.dynamicAspectRatio            // landscape-соотношение с квадратного сенсора
AVCaptureDeviceRotationCoordinator   // обязательно, обновляется при переезде между дисплеями
photoOutput.isCameraSensorOrientationCompensationEnabled = false
Последняя строка важна: на Duo компенсация включена на всех фронтальных камерах и стоит производительности. После внедрения rotation coordinator её нужно выключить.

✅ Готово, когда камера корректно переключается при складывании и раскладывании, зеркалирование правильное во всех четырёх комбинациях, превью не обрезано и не леттербоксное.

🖼 Кадры из сессий Apple (8)
<b>Preview layer.</b> <code>AVCaptureDeviceRotationCoordinator</code> держит превью правильно ориентированным.
Preview layer. AVCaptureDeviceRotationCoordinator держит превью правильно ориентированным. ▶ 7:15
<b>Inner ultrawide camera</b> — первая under-display камера на iPhone.
Inner ultrawide camera — первая under-display камера на iPhone. ▶ 0:55
<b>Сравнение камер.</b> Разрешение, framerate и поддержка depth отличаются — не считайте их взаимозаменяемыми.
Сравнение камер. Разрешение, framerate и поддержка depth отличаются — не считайте их взаимозаменяемыми. ▶ 2:45
<b><code>AVCaptureDeviceDirectionCoordinator</code></b> — сообщает, какая камера смотрит в нужную сторону.
AVCaptureDeviceDirectionCoordinator — сообщает, какая камера смотрит в нужную сторону. ▶ 4:05
<b>Forward / backward</b> вместо front / back: при складывании «перёд» меняется.
Forward / backward вместо front / back: при складывании «перёд» меняется. ▶ 4:45
<b>Outer и inner display</b> имеют собственные направления.
Outer и inner display имеют собственные направления. ▶ 5:25
<b>Конкурентность.</b> Coordinator и descriptor — на main actor, session — на camera actor, между ними Sendable.
Конкурентность. Coordinator и descriptor — на main actor, session — на camera actor, между ними Sendable. ▶ 6:15
<b><code>isCameraSensorOrientationCompensationEnabled = false</code></b> — когда нужна ручная компенсация ориентации сенсора.
isCameraSensorOrientationCompensationEnabled = false — когда нужна ручная компенсация ориентации сенсора. ▶ 8:45

F15 Игры

Суть. Можно залочить ориентацию, но заполнять экран обязательно во всех позах.

📎 Референсы
🎨 Design
  • Можно залочить portrait или landscape — но обязательно заполнять экран во всех позах.
  • Приоритет решений: изменить aspect ratio → letterbox/pillarbox только если иначе никак.
  • Если чёрных полос не избежать — заполните их артвортом, чтобы опыт ощущался полноэкранным.
  • При ресайзе держите размеры текста и контролов постоянными — иначе UI «дышит» и становится неуправляемым.
⚙️ Dev

Те же принципы resizability; избегайте привязки к конкретному экрану.

✅ Готово, когда нет ни одной позы с чёрными полосами без артворка, а размер hit-таргетов стабилен.

Часть 6 · Процесс

F16 Инструменты и тестовая матрица

📎 Референсы
⚙️ Dev — стартовый минимум
  1. Xcode 27.1
  2. Сборка против iOS 27.1 SDK
  3. Симулятор iPhone Duo в Device Hub — кнопки снизу: open / close / rotate / fold
  4. Агентский скилл Xcode app-resizability (бывший app-modernization, теперь поддерживает SwiftUI и Duo). Промпт: «Make my app follow all resizability best practices». В частности автоматически заменяет UIScreen.main.scaletraitCollection.displayScale.
Уровни поддержки по версии SDK — чтобы понимать, где вы сейчас
Собрано сЗакрытое состояниеОткрытое состояние
до iOS 27.0только область слева от status bar и камеры«классический» iPhone-размер с чёрными полями
iOS 27.0+ заходит в зону status barрасширяется горизонтально, но с padding по краям
iOS 27.1edge-to-edge, бары вертикальноedge-to-edge на весь внутренний экран
🎨 Design — минимальный сет экранов в макетах

Для каждого ключевого экрана:

  1. Внешний дисплей, portrait
  2. Внешний дисплей, landscape (здесь чаще всего overflow)
  3. Внутренний дисплей, portrait — горизонтальные бары!
  4. Внутренний дисплей, landscape — вертикальные бары
  5. Внутренний, частично сложен — состояние с division region
  6. Split View, левая половина — контролы слева
  7. Состояние максимального overflow

Плюс, где применимо: tabletop-макет, состояние с активной внутренней камерой, состояние с PiP сверху.

✅ Общая тестовая матрица. 6 поз × (обычный / Split View левый / Split View правый) × RTL × Reduce Transparency.

🖼 Кадры из сессий Apple (2)
<b>Device Hub в Xcode 27.1</b> — симуляция всех поз.
Device Hub в Xcode 27.1 — симуляция всех поз. ▶ 7:55
<b>Минимальные требования:</b> Xcode 27.1 и Device Hub для прогона по всем позам.
Минимальные требования: Xcode 27.1 и Device Hub для прогона по всем позам. ▶ 9:45

F17 Чеклист миграции

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

🎨 Design
⚙️ Dev

⚠️ Топ-5 ловушек

#ЛовушкаЧья зонаПоследствие
1Допущение о симметричных safe area insets⚙️ Devсамый частый баг; контент съезжает или обрезается
2Кастомно созданный UIToolbar⚙️ Devмолча не получает вертикальную раскладку, ошибки нет
3Запрос нового окна на внешнем дисплее⚙️ Dev + 🎨 Designпровалится; на iPad такого ограничения нет
4Layout, привязанный к углу шарнира вместо reserved regions⚙️ Devломается в Split View и на внешнем дисплее
5Функция, доступная только в одной позе🎨 Designпрямо запрещено HIG; люди складывают устройство постоянно

Приложение

A1 Указатель схем Apple

Все схемы живут на странице HIG: Designing for iPhone Duo. В этом документе они встроены в соответствующие разделы.

Что показываетРаздел HIGГде в документе
Шесть поз устройстваDevice posesБаза, F9
Внешний дисплей: шарнир и камераAnatomyБаза
Внутренний дисплей: шарнир и камераAnatomyБаза
Home Screen, внешний и внутреннийIntroБаза
Зона внешней камерыReserved regionsF2, F5
Зона сгиба и внутренней камерыReserved regionsF5
Notes: открыт → сложен (панели 50/50)Reserved regionsF5
Split arrangementArrangement viewsF6
Overlay arrangementArrangement viewsF6
Mail: внешний и внутреннийBest practicesF7
Состав вертикальной полосыVertical controlsF3
Split View: контролы у внешних краёвVertical controlsF10
Calculator на всю ширину (4 колонки → 5)Vertical controlsF2
Контролы рядом со своей панелью (Mail)Vertical controlsF3
Сжат toolbar / сжат tab barVertical controlsF4

A2 Источники

МатериалДлит.ФокусСсылки
Design for iPhone Duo10:45дизайн-принципы, позы, боковые контролыYouTube · Apple 111466
Prepare your app for iPhone Duo10:10SDK, layout, safe areas, инструментыYouTube
Strike a pose with adaptive layouts18:01reserved regions, displacement, ArrangementViewYouTube · Apple 111463
Raise the bar with iPhone Duo15:42вертикальные бары, контент, overflowYouTube · Apple 111462
Leverage multiple displays and scenes07:18hinge, multitasking, scene accessoriesYouTube
Build a great camera experience09:27AVFoundation, две фронтальные камерыYouTube
HIG: Designing for iPhone Duoнормативные правилаdeveloper.apple.com
Apple Design Resourcesшаблоны, гриды, safe areasdeveloper.apple.com
Ничего не найдено. Попробуйте другой запрос.