Симулятор iPhone Duo я запустил 13 октября, когда готовил к выходу свой дневник Lanternly. Сам симулятор оказался полезным, а путь к нему — нет. В обычном Xcode 27.0 устройства Duo нет совсем. Рантайм качается без единой строки прогресса. Вместо привычного Simulator теперь DeviceHub, и половину нужных действий из командной строки сделать нельзя.
Здесь собрано всё, что пригодилось на этом пути: как добраться до симулятора, как снимать оба экрана, как складывать и поворачивать устройство из скрипта и как получить скриншоты Duo для App Store. Что именно пришлось переделать в самом приложении, я разобрал в статье о проблемах адаптации под iPhone Duo.
Где взять симулятор iPhone Duo#
Устройство Duo есть только в Xcode 27.1. Я поставил Release Candidate (сборка 27A9275) рядом с релизным 27.0, не заменяя его, и вызываю нужный Xcode через DEVELOPER_DIR:
export DEVELOPER_DIR=~/Downloads/Xcode.app/Contents/Developer # 27.1 RC
xcrun simctl list devicetypes | grep -i duo
#> iPhone Duo (com.apple.CoreSimulator.SimDeviceType.iPhone-Duo)В Xcode 27.0 та же команда ничего не находит. В подсказках поиска есть запрос «iPhone Duo simulator missing», и первый ответ на него такой: тип устройства приходит вместе с Xcode 27.1, а не с рантаймом.
Второй ответ — рантайм iOS 27.1. Без него устройство в списке есть, а создать его не на чем:
xcodebuild -downloadPlatform iOS
#> Downloading iOS 27.1 Simulator (24A94232) (arm64): 22,0% (1,73 GB of 7,85 GB)Это 7,85 ГБ. Я сначала запустил загрузку с | tail -20, и она выглядела зависшей: tail ничего не печатает, пока команда не закончится. Пустил вывод в файл и увидел нормальный прогресс. Проверка, что всё встало:
xcrun simctl runtime list | grep 27.1
#> iOS 27.1 (24A94232) - … (Ready)Создать устройство и найти оба экрана#
UDID=$(xcrun simctl create "iPhone Duo" \
com.apple.CoreSimulator.SimDeviceType.iPhone-Duo \
com.apple.CoreSimulator.SimRuntime.iOS-27-1)
xcrun simctl boot "$UDID"
xcrun simctl io "$UDID" enumerateenumerate печатает длинный список портов. Нужны два экрана с типом Integrated:
| Экран | Имя для --display | Размер в пикселях |
|---|---|---|
| Внешний (сложен) | primary | 1398 × 2034 |
| Внутренний (разложен) | primary-1 | 2007 × 2853 |
Это ровно размеры слотов скриншотов iPhone Duo в App Store Connect. Скриншот каждого экрана снимается отдельно:
xcrun simctl io "$UDID" screenshot --display=primary outer.png
xcrun simctl io "$UDID" screenshot --display=primary-1 inner.pngСразу после загрузки устройство сложено. Внутренний экран при этом снимается чёрным прямоугольником, и это не ошибка.
DeviceHub вместо Simulator#
В Xcode 27 приложение Simulator заменено на DeviceHub: Xcode.app/Contents/Applications/DeviceHub.app. Внизу окна у него ряд кнопок: поворот и три позы — сложен, книга, разложен. Поза Duo меняется только там.
Я рассчитывал на simctl, и не зря проверил: команд поворота и сгиба в нём нет. Есть io screenConfig, который умеет выключать отдельный экран:
xcrun simctl io "$UDID" screenConfig --display=primary power off
#> Screen 1 power state set to offПозу это не меняет. Внешний экран гаснет, а внутренний остаётся чёрным, потому что устройство по-прежнему сложено.
Как нажимать кнопки DeviceHub из скрипта#
Следующая мысль была управлять DeviceHub через AppleScript. Не вышло: System Events видит процесс, но у него 0 окон и нет доступной строки меню, хотя Finder в той же сессии читается нормально. Сработали обычные события мыши через CoreGraphics. Утилита на Swift в десять строк:
import CoreGraphics
import Foundation
// click x y — клик в точке экрана (координаты в пунктах)
let a = CommandLine.arguments
let p = CGPoint(x: Double(a[1])!, y: Double(a[2])!)
let src = CGEventSource(stateID: .hidSystemState)
CGEvent(mouseEventSource: src, mouseType: .mouseMoved, mouseCursorPosition: p, mouseButton: .left)?.post(tap: .cghidEventTap)
usleep(150_000)
CGEvent(mouseEventSource: src, mouseType: .leftMouseDown, mouseCursorPosition: p, mouseButton: .left)?.post(tap: .cghidEventTap)
usleep(80_000)
CGEvent(mouseEventSource: src, mouseType: .leftMouseUp, mouseCursorPosition: p, mouseButton: .left)?.post(tap: .cghidEventTap)Собирается swiftc -O click.swift -o click. Координаты я беру со снимка screencapture -x: на Retina-экране он в пикселях, поэтому делю на 2 и получаю пункты.
Несколько мелочей, на которые ушло время:
- Перед кликом окно нужно вывести вперёд (
tell application "DeviceHub" to activate) и подождать секунду-две. Без паузы первый клик иногда уходил в пустоту. - В портрете иконки поз нарисованы повёрнутыми. Порядок кнопок тот же, но узнать их по картинке сложнее.
- Если корпус повёрнут, после складывания внешний экран окажется в альбоме. Я жму «повернуть», пока снимок
--display=primaryне станет шириной 1398. - Меню DeviceHub в строке меню кликается так же:
Controls → Home,Device → Touch ID → Enrolled. Горячие клавиши через System Events до него не доходили.
Поворот без DeviceHub#
Для iPhone и iPad поворачивать удобнее из самого приложения. Я добавил отладочный флаг запуска, который просит у сцены нужную ориентацию:
#if DEBUG
// -orientation landscape,portrait — по шагу каждые 4 с после старта
static func applyOrientationFlag() {
let args = CommandLine.arguments
guard let i = args.firstIndex(of: "-orientation"), i + 1 < args.count else { return }
for (step, name) in args[i + 1].split(separator: ",").enumerated() {
let mask: UIInterfaceOrientationMask = name == "portrait" ? .portrait : .landscapeRight
DispatchQueue.main.asyncAfter(deadline: .now() + Double(step + 1) * 4) {
for scene in UIApplication.shared.connectedScenes.compactMap({ $0 as? UIWindowScene }) {
scene.requestGeometryUpdate(.iOS(interfaceOrientations: mask))
}
}
}
}
#endifxcrun simctl launch "$UDID" <bundle> -orientation landscape,portrait поворачивает экран туда и обратно, а снимки между шагами показывают, как приложение переживает смену ширины. На iPhone 18 Pro Max это сработало. На iPad под iPadOS 27 приложение живёт в окне, и запрос поворота система игнорирует.
Скриншоты iPhone Duo для App Store#
Размеры слотов совпадают с буферами экранов, так что сырые кадры получаются прямо из simctl io screenshot. Перед съёмкой я привожу устройство в порядок:
xcrun simctl status_bar "$UDID" override --time 9:41 --batteryLevel 100 \
--batteryState discharging --wifiBars 3 --cellularBars 4
xcrun simctl privacy "$UDID" grant microphone <bundle>Значение discharged, которое встречается в старых шпаргалках, simctl отвергает. Допустимы charging, charged и discharging.
Ещё три вещи всплыли только на готовых кадрах:
- Системные запросы. После переустановки приложения на экране записи голоса всплыл запрос доступа к микрофону. Его снимает
simctl privacy … grant. - Touch ID. Пока в меню не включено
Device → Touch ID → Enrolled, экран настроек честно пишет, что биометрия недоступна. Перезагрузка симулятора (например, при смене языка) сбрасывает эту галочку. - Служебные подписи. В отладочной сборке у меня в шапке чата виден источник ответа («· симулятор»), а в настройках — локальный режим хранилища. Флаг запуска
-storeShotsпрячет их только на время съёмки, релизная сборка от этого не меняется.
И главное правило, на котором я однажды ошибся. В слот устройства идут кадры только с этого устройства. До появления симулятора Duo я вписал в его размер кадры с iPad, и их пришлось удалить. Скриншот обещает, что человек увидит на своём экране, а на Duo он видит другую раскладку. Размеры и форматы остальных материалов App Store собраны в руководстве по Creative Assets.
Что не получилось#
Split View жестом. На внутреннем экране Duo, как на iPad, второе приложение перетаскивается из Dock. Мышиный свайп через CGEvent либо открывал переключатель приложений, если был длинным, либо ничего не делал, если короткий. Этот сценарий остаётся на ручную проверку. Логика узкой половины экрана частично покрыта тем, что на внешнем экране приложение и так работает в узком окне, но это вывод, а не проверка.
Позы из командной строки. Сгиб, книга и разложенное устройство переключаются только кнопками DeviceHub. Если у вас CI на симуляторах, проверки поз туда пока не перенести.
Если тестировать Duo с нуля, порядок такой: Xcode 27.1 рядом с основным, рантайм 27.1 с выводом в файл, simctl create и enumerate, а для поз — DeviceHub и маленькая утилита кликов. Что в приложении сломалось на этих экранах и как я это чинил, рассказано в статье про адаптацию. Общая теория по устройству — в гайде по iPhone Duo.



