Arranqué el simulador iPhone Duo el 13 de octubre, mientras preparaba el lanzamiento de mi diario Lanternly. El simulador en sí resultó útil. Llegar hasta él, no tanto. El Xcode 27.0 normal no tiene el dispositivo Duo. El runtime se descarga sin una sola línea de progreso. Simulator ya no existe: lo sustituye DeviceHub, y la mitad de lo que necesitaba no se puede hacer desde la línea de comandos.
Aquí está todo lo que me sirvió: cómo llegar al simulador, cómo capturar las dos pantallas, cómo plegar y girar el dispositivo desde un script y cómo sacar capturas de iPhone Duo para el App Store. Lo que tuve que cambiar en la propia app lo cuento en el artículo sobre los problemas de adaptación a iPhone Duo.
Dónde conseguir el simulador iPhone Duo#
El dispositivo Duo solo existe en Xcode 27.1. Instalé la Release Candidate (build 27A9275) junto al 27.0 estable, sin reemplazarlo, y elijo el Xcode con 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)En Xcode 27.0 el mismo comando no encuentra nada. En las sugerencias de búsqueda aparece «iPhone Duo simulator missing», y la primera respuesta es esta: el tipo de dispositivo llega con Xcode 27.1, no con el runtime.
La segunda respuesta es el runtime de iOS 27.1. Sin él, el dispositivo aparece en la lista, pero no hay nada sobre lo que crearlo:
xcodebuild -downloadPlatform iOS
#> Downloading iOS 27.1 Simulator (24A94232) (arm64): 22,0% (1,73 GB of 7,85 GB)Son 7,85 GB. La primera vez lo pasé por | tail -20 y parecía colgado: tail no imprime nada hasta que el comando termina. Con la salida a un archivo vi el progreso normal. Para comprobar que se instaló:
xcrun simctl runtime list | grep 27.1
#> iOS 27.1 (24A94232) - … (Ready)Crear el dispositivo y encontrar las dos pantallas#
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 imprime una lista larga de puertos. Te interesan las dos pantallas de tipo Integrated:
| Pantalla | Nombre para --display | Tamaño en píxeles |
|---|---|---|
| Exterior (plegado) | primary | 1398 × 2034 |
| Interior (desplegado) | primary-1 | 2007 × 2853 |
Son exactamente los huecos de capturas de iPhone Duo en App Store Connect. Cada pantalla se captura por separado:
xcrun simctl io "$UDID" screenshot --display=primary outer.png
xcrun simctl io "$UDID" screenshot --display=primary-1 inner.pngJusto después de arrancar, el dispositivo está plegado y la pantalla interior sale como un rectángulo negro. No es un error.
DeviceHub en lugar de Simulator#
En Xcode 27 la app Simulator se sustituye por DeviceHub: Xcode.app/Contents/Applications/DeviceHub.app. Abajo en su ventana hay una fila de botones: girar y tres posturas, plegado, libro y abierto. La postura del Duo solo se cambia ahí.
Contaba con simctl y lo comprobé: no tiene comandos para girar ni plegar. Tiene io screenConfig, que apaga una pantalla concreta:
xcrun simctl io "$UDID" screenConfig --display=primary power off
#> Screen 1 power state set to offEso no cambia la postura. La pantalla exterior se apaga y la interior sigue negra, porque el dispositivo sigue plegado.
Pulsar los botones de DeviceHub desde un script#
La siguiente idea fue controlar DeviceHub con AppleScript. No salió: System Events ve el proceso, pero con 0 ventanas y sin barra de menús accesible, mientras que Finder se lee bien en la misma sesión. Lo que sí funcionó fueron los eventos de ratón de CoreGraphics. Una utilidad en Swift de diez líneas:
import CoreGraphics
import Foundation
// click x y — clic en un punto de la pantalla (coordenadas en puntos)
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)Se compila con swiftc -O click.swift -o click. Las coordenadas las saco de una captura screencapture -x: en una pantalla Retina está en píxeles, así que divido entre 2 para tener puntos.
Algunos detalles que me costaron tiempo:
- Antes del clic hay que traer la ventana al frente (
tell application "DeviceHub" to activate) y esperar uno o dos segundos. Sin la pausa, el primer clic a veces se perdía. - En vertical los iconos de postura se dibujan girados. El orden de los botones es el mismo, solo cuesta más reconocerlos.
- Si el cuerpo está girado, al plegar la pantalla exterior queda en horizontal. Pulso girar hasta que una captura de
--display=primarymide 1398 de ancho. - La barra de menús de DeviceHub acepta los mismos clics:
Controls → Home,Device → Touch ID → Enrolled. Los atajos de teclado enviados con System Events no le llegaban.
Girar sin DeviceHub#
En iPhone y iPad es más cómodo girar desde la propia app. Añadí un flag de depuración que pide a la escena una orientación:
#if DEBUG
// -orientation landscape,portrait — un paso cada 4 s tras el arranque
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 gira la pantalla de ida y vuelta, y las capturas entre pasos muestran cómo la app aguanta el cambio de ancho. En el iPhone 18 Pro Max funcionó. En iPad con iPadOS 27 la app vive en una ventana y el sistema ignora la petición.
Capturas de iPhone Duo para el App Store#
Los tamaños de los huecos coinciden con los buffers de pantalla, así que las capturas en bruto salen directamente de simctl io screenshot. Antes de capturar dejo el dispositivo en orden:
xcrun simctl status_bar "$UDID" override --time 9:41 --batteryLevel 100 \
--batteryState discharging --wifiBars 3 --cellularBars 4
xcrun simctl privacy "$UDID" grant microphone <bundle>El valor discharged, que aparece en chuletas antiguas, simctl lo rechaza. Los válidos son charging, charged y discharging.
Tres cosas más solo aparecieron en las capturas terminadas:
- Avisos del sistema. Tras reinstalar la app, en la pantalla de grabación de voz salió el aviso de acceso al micrófono.
simctl privacy … grantlo elimina. - Touch ID. Mientras no se marque
Device → Touch ID → Enrolled, los ajustes dicen con honestidad que la biometría no está disponible. Reiniciar el simulador, por ejemplo para cambiar de idioma, desmarca la opción. - Etiquetas de depuración. En mi build de depuración la cabecera del chat muestra de dónde viene la respuesta («· simulador») y los ajustes muestran el modo de almacenamiento local. El flag
-storeShotslas oculta solo durante la sesión de capturas. La build de producción no cambia.
Y la regla principal, que una vez me salté. En el hueco de un dispositivo solo van capturas de ese dispositivo. Antes de que existiera el simulador Duo encajé capturas de iPad en su tamaño y tuve que borrarlas. Una captura promete lo que la persona verá en su pantalla, y en un Duo verá otra disposición. Los tamaños y formatos del resto de recursos del App Store están en la guía de Creative Assets.
Lo que no salió#
Split View con gesto. En la pantalla interior del Duo, como en iPad, la segunda app se arrastra desde el Dock. Un deslizamiento de ratón con CGEvent abría el selector de apps si era largo o no hacía nada si era corto. Este caso queda para una comprobación manual. La lógica de media pantalla estrecha queda cubierta en parte porque la app ya funciona en una ventana estrecha en la pantalla exterior, pero eso es una deducción, no una prueba.
Posturas desde la línea de comandos. Plegado, libro y abierto solo se cambian con los botones de DeviceHub. Si tu CI corre en simuladores, las pruebas de postura todavía no pueden ir ahí.
Si empiezas a probar un Duo desde cero, el orden es: Xcode 27.1 junto al principal, el runtime 27.1 con la salida a un archivo, simctl create y enumerate, y para las posturas, DeviceHub más una pequeña utilidad de clics. Lo que se rompió en la app en estas pantallas y cómo lo arreglé está en el artículo sobre la adaptación. La teoría general del dispositivo, en la guía de iPhone Duo.



