Во всех шести Tech Talks про iPhone Duo нет ни строчки Objective-C. Это ожидаемо — Apple показывает новое на Swift и SwiftUI. Но реальность индустрии другая: банковские приложения, мессенджеры, всё, что писалось с 2010-х и пережило пять редизайнов, несёт в себе ObjC-ядро. За восемь лет в iOS я видел достаточно таких кодовых баз, чтобы знать: «перепишем на Swift, а потом адаптируем» — это план, который не случится никогда. Адаптировать придётся то, что есть.
Хорошая новость: почти всё, что нужно для Duo, живёт в UIKit, а UIKit по-прежнему говорит на Objective-C. В этой статье — что доступно напрямую, что через UIKit-аналоги, и в каких двух местах без Swift-прослойки не обойтись. Сигнатуры — из Tech Talks сентября 2026; до выхода стабильного Xcode 27.1 сверяйте написание с заголовками SDK.
Шаг 0: пересборка и аудит#
Первое действие не зависит от языка: собрать проект Xcode 27.1 против iOS 27.1 SDK. Без этого не будет ни edge-to-edge, ни вертикальных баров, ни новых API — приложение просто получит поля по краям экрана.
Дальше — grep. Каждая находка по этим паттернам — потенциальный дефект на Duo:
[UIScreen mainScreen] → неоднозначен на двух экранах
interfaceOrientation → внутренний дисплей игнорирует ориентации
UI_USER_INTERFACE_IDIOM() → Duo не новая идиома, ветки по идиоме ломаются
userInterfaceIdiom
safeAreaInsets.left * 2 → инсеты асимметричны
хардкод ширин (390, 428, ...) → ширина меняется движением рукиВ старых кодовых базах [[UIScreen mainScreen] bounds] встречается десятками — в вычислениях фреймов, в конфигурации коллекций, в кэшах размеров. Замены:
// Масштаб — из trait collection, не из экрана
CGFloat scale = self.traitCollection.displayScale;
// Если экран действительно нужен (редко) — через сцену окна
UIScreen *screen = self.view.window.windowScene.screen;
// Размер для layout — bounds собственной view с учётом safe area
CGRect content = UIEdgeInsetsInsetRect(self.view.bounds,
self.view.safeAreaInsets);Size classes вместо ориентаций — по-старому, но всерьёз#
API trait collections не изменился со времён iOS 8 — изменилась цена его игнорирования. Приложение, залоченное в портрет, на внутреннем дисплее Duo будет вращаться независимо от supportedInterfaceOrientations, так что вся логика «в ландшафте показываем иначе» должна переехать на size classes:
- (void)traitCollectionDidChange:(UITraitCollection *)previous {
[super traitCollectionDidChange:previous];
if (previous.horizontalSizeClass == self.traitCollection.horizontalSizeClass) {
return;
}
BOOL isWide = self.traitCollection.horizontalSizeClass ==
UIUserInterfaceSizeClassRegular;
// compact — внешний экран Duo (и все обычные iPhone),
// regular — внутренний экран (и iPad)
[self rebuildLayoutForWideMode:isWide];
}На iOS 17+ вместо переопределения traitCollectionDidChange: можно использовать зарегистрированное наблюдение (registerForTraitChanges:) — оно тоже экспонировано в Objective-C.
Отдельный пункт аудита — Auto Layout против ручных фреймов. Констрейнты к safeAreaLayoutGuide переживут Duo без правок; ручной расчёт фреймов через self.view.frame.size.width со смещениями — нет. Приоритет миграции стоит отдавать вторым.
Бары: вычистить самодельное, разметить системное#
Правило из Tech Talk 111462 бьёт по legacy-коду больнее всего: в вертикальной раскладке участвуют только системные контейнеры — UINavigationController и UITabBarController. Кастомный UIToolbar, добавленный сабвью, кастомная «шапка» из UIView с кнопками — всё это останется горизонтальным и будет конфликтовать с системным вертикальным баром.
Если бары системные, разметка нового поведения делается целиком из Objective-C:
// Главное действие — закрепить в вертикальном баре
self.navigationItem.pinnedTrailingGroup =
[UIBarButtonItemGroup fixedGroupWithRepresentativeItem:nil
items:@[sendButton]];
// Кастомный back/close — ведущий item; отключить дополнение системной кнопкой
self.navigationItem.leftItemsSupplementBackButton = NO;
// Второстепенное — в overflow-меню
self.navigationItem.additionalOverflowItems =
[UIDeferredMenuElement elementWithProvider:^(void (^completion)(NSArray *)) {
completion(@[self.shareAction, self.exportAction]);
}];
// Приоритеты: что первым уходит в overflow при нехватке места
filterButton.visibilityPriority = UIBarButtonItemVisibilityPriorityLow;
// Бейдж вместо текстового счётчика
inboxButton.badge = [UIBarButtonItemBadge countBadgeWithInteger:7];Вертикальный бар узкий, элементы в нём — только иконки. Текстовую кнопку «Отправить всё» или segmented control туда не затолкать — такие элементы остаются в navigation bar. А опт-аут для конкретного контроллера — переопределение preferredVerticalBarBehavior.
Сцены: долг, который пора вернуть#
Многие ObjC-приложения так и живут на AppDelegate-центричном жизненном цикле, отложив миграцию на сцены «до лучших времён». Duo эти времена назначил: это первый iPhone с несколькими окнами одного приложения и Split View для всех. Сама миграция стандартная (UIApplicationSceneManifest с UIApplicationSupportsMultipleScenes в Info.plist, UIWindowSceneDelegate вместо связки с AppDelegate) — Duo-специфичных ключей нет.
Новое поведение одно, но коварное: окна создаются только на внутреннем дисплее. На закрытом устройстве запрос сцены падает:
UISceneSessionActivationRequest *request =
[UISceneSessionActivationRequest requestWithRole:UIWindowSceneSessionRoleApplication];
[UIApplication.sharedApplication activateSceneSessionForRequest:request
errorHandler:^(NSError *error) {
// Закрытое устройство: новых окон нет — показываем в текущем окне
[self openDocumentInCurrentWindow];
}];Кнопки «открыть в новом окне» в вашем UI должны либо использовать системный activation-аффорданс UIWindowScene (он скрывается сам, когда окна недоступны), либо честно обрабатывать ошибку.
Reserved regions и шарнир из Objective-C#
Геометрия сгиба и камер доступна на уровне UIView:
NSArray<UIViewReservedRegion *> *folds =
[self.view reservedRegionsWithKind:UIViewReservedRegionKindDivision
options:0];
if (folds.count > 0) {
CGRect foldFrame = folds.firstObject.frame;
// Например: не ставить floating-кнопку в этот прямоугольник
}UIHingeInteraction — из семейства UIInteraction, то есть тоже ObjC-совместим: [view addInteraction:]. Но прежде чем тянуться к углу шарнира, стоит спросить, нужен ли он вообще: layout от угла Apple прямо запрещает, а системные компоненты (sheets, alerts, меню, системные бары) обходят сгиб без вашего участия.
Где всё-таки нужен Swift#
Честная граница выглядит так. SwiftUI-эксклюзивы — onHingeChange, ArrangementView, .sceneAccessory с CameraCaptureAccessory — из Objective-C недоступны, но у первых двух есть полноценные UIKit-аналоги (UIHingeInteraction, UIArrangementViewController), и для большинства legacy-приложений их достаточно.
Swift-прослойка понадобится в двух случаях. Первый — камерные приложения с явным управлением модулями: AVCaptureDeviceDirectionCoordinator изолирован на main actor и передаёт устройство через AVCaptureDeviceDescriptor — работать с этим из ObjC формально можно, но модель акторов Swift Concurrency в ObjC не существует, и безопаснее оформить координацию камер отдельным Swift-типом с ObjC-совместимым фасадом. Второй — dual-display-фичи вроде телесуфлёра на внешнем экране: CameraCaptureAccessory заявлен через SwiftUI scene accessory API.
Паттерн один и тот же: маленький @objc-класс на Swift, который инкапсулирует новый API и выставляет в legacy-код делегат или блок. Так уже переживали HealthKit, WidgetKit и App Intents — Duo в этом смысле ничего нового не изобрёл.
План на квартал#
Если свести к списку, адаптация ObjC-приложения выглядит так:
- Пересборка с iOS 27.1 SDK, прогон в симуляторе Duo (Device Hub).
- Grep-аудит:
mainScreen, ориентации, идиомы, симметричные инсеты, хардкод ширин. - Миграция самодельных баров на системные контейнеры + разметка pinned/overflow.
- Сцены: манифест,
UIWindowSceneDelegate, обработка отказа создания окна. - Точечные Swift-прослойки для камеры и dual-display, если они нужны продукту.
Пункты 1–2 — дни, 3–4 — недели и зависят от накопленного долга, пункт 5 нужен не всем. До 23 октября, когда устройство окажется в руках пользователей, время на первые два пункта есть у любой команды.



