Первый вопрос, который я услышал от коллег после анонса iPhone Duo: «а угол сгиба можно читать?» Можно. Apple отдаёт и дискретный статус шарнира, и непрерывный угол — в SwiftUI и в UIKit. Но в том же Tech Talk, где этот API показывают, звучит жёсткое ограничение: шарнир — для эффектов и интеракций, а не для layout. Разница принципиальная, и на ней построена вся вторая половина новых API. Разбираю по порядку: hinge API, reserved regions, arrangements и камеры — с тем, что доступно из Objective-C, и тем, для чего понадобится Swift.
Как и в первой статье серии, напомню: сигнатуры взяты из Tech Talks 111463–111465, Xcode 27.1 ещё в бете — перед продом сверяйте с заголовками SDK.
onHingeChange: три статуса и живой угол#
SwiftUI-модификатор onHingeChange вызывает замыкание с предыдущим и текущим контекстом. Канонический пример Apple — гитарное приложение, где угол сгиба работает как рычаг тремоло:
struct InstrumentView: View {
/// 0 — без изгиба, 1 — максимальный
@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
}
}
}
}Здесь два обязательных элемента, которые легко упустить:
context.hinge— Optional.nilозначает, что у устройства нет шарнира — тот же бинарь работает на обычных iPhone. Без guard код упадёт на большинстве устройств в мире.- Ветка
elseсо сбросом. Статусов три:.closed,.partiallyOpen,.fullyOpen. Если сбрасывать эффект только по.closed, то при раскрытии устройства плашмя эффект «залипнет» на последнем угле.
В UIKit то же самое делает UIHingeInteraction — по конвенции семейства UIInteraction он добавляется к view, и это означает доступность из Objective-C:
// Форма API — по семейству UIInteraction; точную сигнатуру
// хендлера сверьте с заголовками iOS 27.1 SDK
UIHingeInteraction *hinge = [[UIHingeInteraction alloc] init];
[self.effectView addInteraction:hinge];Важно, чего здесь нет: нотификаций. Ни один официальный источник не называет NSNotification для шарнира — наблюдение только через interaction/модификатор. Если где-то встретите UIDeviceHingeDidChangeNotification — это выдумка.
Почему шарнир — не для layout#
Соблазн очевиден: читать угол и раскладывать интерфейс в зависимости от него. Apple прямым текстом говорит этого не делать. Угол — непрерывный сенсорный поток; layout, привязанный к нему, дёргается во время сгибания и ломается на всех устройствах без шарнира. Для структурных решений есть два отдельных API.
Reserved regions (iOS 27.1) — области view, «занятые» железом. Два вида: .division — сгиб, делит область (активен только пока устройство частично сложено; в плоском состоянии регион неактивен и имеет нулевую ширину); .occlusion — камеры, перекрывают область.
// SwiftUI — из GeometryProxy
let folds = proxy.reservedRegions(kind: .division)
let cameras = proxy.reservedRegions(kind: .occlusion)
// Для стабильных решений: учитывать сгиб, даже когда он неактивен
let allFolds = proxy.reservedRegions(kind: .division, options: .includeInactive)// Objective-C: тот же метод на UIView, регионы — UIViewReservedRegion
NSArray<UIViewReservedRegion *> *folds =
[self.view reservedRegionsWithKind:UIViewReservedRegionKindDivision
options:0];
for (UIViewReservedRegion *region in folds) {
// region.frame — прямоугольник сгиба в координатах view
}Опция .includeInactive решает неочевидную проблему: если сетка перестраивается каждый раз, когда регион сгиба появляется и исчезает, интерфейс «перетасовывается» при каждом движении шарнира. Стабильное решение — например, всегда чётное число колонок на устройстве, у которого сгиб в принципе есть.
Хорошая новость: системные контейнеры несут fold avoidance бесплатно. NavigationSplitView, TabView, sheets, alerts, меню и popovers сами отодвигают интерактивные элементы от кривизны сгиба. Единственное исключение — скроллируемый контент: статье или ленте разрешено течь под сгибом, двигать её не нужно.
ArrangementView: split или overlay#
Для пары view «между навигацией и контентом» появился отдельный контейнер. В SwiftUI — ArrangementView, в UIKit — UIArrangementViewController:
ArrangementView {
PlayerView()
} secondary: {
UpNextView()
}
.arrangementViewStyle(.split)UIArrangementViewController *arrangement = [[UIArrangementViewController alloc] init];
[arrangement setViewController:playerVC forPlacement:UIArrangementPlacementPrimary];
[arrangement setViewController:upNextVC forPlacement:UIArrangementPlacementSecondary];Два стиля с разной семантикой. Split делит bounds и ничего не перекрывает — main/detail вроде плеера с транскриптом; приятный бонус против iPad-поведения: при закрытии secondary первичная view не перецентрируется через весь экран, а остаётся у сгиба. Overlay — стек, который при сгибе раскладывается бок о бок; это foreground-контролы над скроллируемым контентом. Эвристика миграции простая: то, что было HStack/VStack — split; то, что было ZStack — overlay.
И два запрета из Tech Talk: не класть NavigationSplitView внутрь arrangement (контейнер не даёт навигационной инфраструктуры) и не класть arrangement внутрь List/ScrollView. Если нужны схлопывающиеся колонки — это по-прежнему UISplitViewController, arrangement не для того.
Камеры: position == .front больше не значит «смотрит на вас»#
У Duo впервые на iPhone две фронтальные камеры: внешняя (4K/120) и внутренняя подэкранная (1080p/60). Обе честно рапортуют position == .front — но дисплеи могут смотреть в противоположные стороны, так что «фронтальная» камера не обязательно смотрит на пользователя. На этом факте ломается вся логика зеркалирования, написанная до 2026 года.
Простой путь — виртуальная фронтальная камера: системное устройство, которое само переключается между внутренним и внешним модулем при открытии и закрытии. Дискавери как раньше (AVCaptureDeviceDiscoverySession с position .front — работает и из Objective-C), но отдаёт оно только пересечение возможностей обеих камер: 1080p, 60 fps, без depth. Для большинства приложений этого достаточно, и переключение модулей — не ваша забота.
Если нужны полные возможности конкретного модуля, появились типы .builtInInnerUltraWideCamera и .builtInOuterUltraWideCamera — но тогда переключение на вас, и тут появляется главный новый инструмент — AVCaptureDeviceDirectionCoordinator из AVKit:
let coordinator = AVCaptureDeviceDirectionCoordinator(
view: previewView,
deviceTypes: [.builtInInnerUltraWideCamera, .builtInOuterUltraWideCamera]
) { descriptor in
// Дескриптор — Sendable; сам AVCaptureDevice строим на камерном акторе
Task { await cameraActor.switchTo(descriptor) }
}Координатор привязан к конкретной view и отвечает на вопрос «какая камера сейчас смотрит в ту же сторону, что этот дисплей». Он изолирован на main actor, а через границу актора передаётся не AVCaptureDevice, а AVCaptureDeviceDescriptor — sendable-представление устройства. В change-хендлере три обязанности: переконфигурировать сессию, пересчитать зеркалирование по направлению, а не по position, и обновить UI.
Ещё две камерные мелочи из Tech Talk 111465: после перехода на AVCaptureDeviceRotationCoordinator отключайте isCameraSensorOrientationCompensationEnabled — на фронталках Duo компенсация включена по умолчанию и станет двойной работой; а dynamicAspectRatio позволяет задействовать квадратный сенсор под ландшафт внутреннего экрана.
Телесуфлёр на втором дисплее#
Камерные приложения получают ещё одну возможность: пока главный UI занимает внутренний дисплей с активной камерной сессией, на внешний можно вывести дополнительный контент — CameraCaptureAccessory. Сценарии из доклада: телесуфлёр для записи видео или что-то весёлое для ребёнка, которого фотографируют.
CameraView(model: model)
.sceneAccessory {
CameraCaptureAccessory(isEnabled: $model.isEnabled) {
TeleprompterView(model: model)
}
.onAvailabilityChange { model.isAvailable = $0 }
}Доступностью управляет система, поэтому onAvailabilityChange — не опция, а обязанность: аксессуар может пропасть в любой момент, когда пользователь сложил устройство.
Итог#
Логика новых API складывается в цельную картину: шарнир — сенсор для эффектов, reserved regions — факты о железе для layout, arrangements — готовые паттерны для пары view, направление камеры — отдельная сущность, не выводимая из position. Из Objective-C доступна вся UIKit/AVFoundation-поверхность; SwiftUI-эксклюзивы (onHingeChange, ArrangementView, sceneAccessory) в ObjC-проекте потребуют либо UIKit-аналогов, либо тонкой Swift-прослойки — про это третья статья серии.



