Saltar al contenido principal
Volver al blogCopiar Markdown
·10 min de lectura
·iosiphone duoswiftuixcuitestagents

Simulador del iPhone Duo: tests de SwiftUI sin ventana

Tests de UI de Rydger en el simulador del iPhone Duo: Cancelar y Guardar desaparecían, SpringBoard se colgaba en bucle y la pantalla interior pedía un apaño →

Óscar Gallego

Ingeniero de software · Málaga

El garaje de Rydger en un iPhone Duo, abierto y cerrado, junto al título UI tests on the iPhone Duo simulator
En este artículo

Respuesta corta: en el simulador del iPhone Duo, Cancelar y Guardar desaparecían de las hojas de mi app en cuanto el formulario hacía scroll. El agente que llevaba lo del Duo probó ocho arreglos y ninguno funcionó. Una línea sí: toolbarVerticalBehavior(.disabled). Por el camino me tocó además un SpringBoard colgándose en bucle, un simulador que me borró mi propio script de limpieza y un test que no conseguía tocar un campo. Va todo abajo, con su arreglo.

La app es Rydger, en SwiftUI y con iOS 18 como versión mínima. El entorno: Xcode 27.2, el runtime de iOS 27.1 y el simulador sin ventana, que es como mis agentes lanzan todos los tests.

Cancelar y Guardar desaparecían en cuanto el formulario hacía scroll

Las pantallas de alta y edición de vehículo de Rydger son una hoja: un NavigationStack, un Form y una barra con .cancellationAction y .confirmationAction. En un simulador de iPhone 17 Pro pasan los dos tests de alta de vehículo. En el Duo fallaban los dos. El que edita la matrícula moría con «Failed to tap Save», y el árbol de accesibilidad del fallo mostraba la barra de navegación vacía.

El agente que llevaba la historia del Duo lo reprodujo con un Form mínimo y, según su propio informe, probó ocho variantes: quitar el fundido de arriba, el indicador de arrastre y el botón Done del teclado, tocar contentMargins, el elemento .principal de la barra, safeAreaInset, presentationSizing(.page) y forzar toolbarVisibility. Ninguna devolvió los botones. Entregó la rama con el bug anotado como pendiente.

Un modificador los devuelve

La búsqueda de Grok me llevó al post de Sarun sobre las hojas en el Duo: con el móvil cerrado y en vertical, iOS pasa los botones de cerrar y confirmar de las hojas a una barra vertical, y ese comportamiento se puede desactivar. Él recomienda desactivarlo en hojas con un solo botón. Las mías tienen dos. En el simulador nunca vi la barra vertical, solo botones que desaparecían al hacer scroll, pero al desactivarla volvieron. El modificador viene en el SDK de iOS 27.2 y funciona desde iOS 27.1:

private extension View {
    /// iPhone Duo (iOS 27.1+) moves a sheet's Cancel and Save into a vertical toolbar,
    /// and on the 27.1 simulator both vanish once the form scrolls: keep the usual bar.
    @ViewBuilder func horizontalToolbar() -> some View {
        if #available(iOS 27.1, *) { toolbarVerticalBehavior(.disabled) } else { self }
    }
}

Con eso puesto en la hoja compartida, en la siguiente pasada Cancelar y Guardar seguían en la barra después del scroll (y=112 en el árbol de accesibilidad). El test de la matrícula siguió fallando, por otro motivo que cuento más abajo. Le pedí a Grok que buscara este bug en X y en la web entre el 10 de septiembre y el 5 de noviembre y no encontró a nadie que lo mencionara. Así que no sé si Apple lo considera un bug o si es el comportamiento previsto.

Simular el pliegue con simctl colgó SpringBoard en bucle

Para capturar la pantalla interior, el agente intentó «abrir» el móvil encendiendo y apagando sus pantallas con xcrun simctl io <udid> screenConfig … power. A partir de ahí, el SpringBoard del simulador se reiniciaba más o menos cada cinco segundos: 14 crash reports entre las 18:18:50 y las 18:20:20, todos menos el primero con una aserción en -[SBMainDisplayConfigurationTransformer transformDisplayConfiguration:], y cada uno con su aviso de macOS en mi pantalla. Una hora después, otro agente lanzó la misma llamada a screenConfig en un segundo simulador de Duo, y nada más hacerlo ese también entró en bucle.

Un simulador de Duo nuevo, en el que nadie usó screenConfig, arrancó sin ventana con simctl boot y no dejó ni un fallo de SpringBoard. A Blake Crosley le pasó algo parecido en septiembre: apagar la pantalla exterior tumbaba la interfaz del sistema del simulador. Nada de screenConfig para simular el pliegue.

Mi propio script de limpieza me borró el simulador

Después, el simulador nuevo se me apagó dos veces y acabó desapareciendo de simctl list. Mi repo tiene un script que limpia los worktrees terminados, y su modo --orphans apaga y borra cualquier simulador que se llame «Rydger Agent » si ese worktree ya no existe. Yo había llamado al de pruebas «Rydger Agent duo-check», y en el mismo Mac había otras sesiones de agentes recogiendo lo suyo. A las 20:35, otra sesión de agentes que limpiaba sus worktrees lo lanzó, y en su salida aparecía «Simulator duo-check»: el nombre cumplía la regla y no existía ningún worktree con ese nombre. Los dos apagados de antes no sé explicarlos. Uno llamado «Duo Capture (Rydger)» ha sobrevivido desde entonces.

No hay forma oficial de plegar el simulador

simctl no tiene ningún comando de postura. Hay un hilo en los foros de Apple que lo pide y Apple no ha contestado. En Xcode 27.2 la ventana del simulador es Device Hub, que tiene los botones de postura, pero mi herramienta de automatización no lograba pulsar dentro de su ventana: no identificaba cuál era.

Lo que funcionó fue duoctl, una herramienta pequeña de código abierto que manda eventos de bisagra al simulador con llamadas HID privadas. Un agente leyó el código antes de ejecutarlo: un script de Python que llama a xcrun y codesign y compila un helper que corre dentro del simulador, sin tocar la red. Funciona sin ventana con Xcode 27.2:

$ duoctl -d <udid> open && duoctl -d <udid> state
"hingeAngle": 180.0, "activeScreen": "inner", "orientation": "landscape", "sizePoints": [951.0, 669.0]

Como usa API privada, la próxima versión de Xcode puede cargárselo. Hoy por hoy es la única forma que he encontrado de abrir el móvil desde un script.

Captura la pantalla interior desde el Mac, no desde XCTest

Con el móvil abierto hay dos trampas más:

  • Capturas. Otro desarrollador cuenta que las capturas de XCTest de la pantalla interior salen en negro, y solo se dibuja la pantalla que está encendida, así que ni las miré. simctl io <udid> screenshot --display=<pantalla encendida> desde el Mac sí funciona, y duoctl screenshot elige la pantalla por ti.
  • Toques. En la pantalla interior, XCUITest nunca marcó mis elementos como pulsables (isHittable), ni girando a vertical, aunque element.tap() sí llegaba.

Así que el test de capturas de la tienda se para en cada pantalla y la foto la hace el Mac:

func snap(_ app: XCUIApplication, _ name: String) {
    if let pause = ProcessInfo.processInfo.environment["SNAP_HOST_PAUSE"].flatMap(Double.init) {
        print("SNAP-HOST-READY \(name)")
        Thread.sleep(forTimeInterval: pause)
    }
    // ... the usual XCTAttachment
}
TEST_RUNNER_SNAP_HOST_PAUSE=4 xcodebuild test-without-building … | while IFS= read -r line; do
  case "$line" in *SNAP-HOST-READY*)
    name=${line##*SNAP-HOST-READY }
    duoctl -d "$SIM" screenshot "out/$name.png";;
  esac
done

En esas pasadas, el agente se saltó la espera a que el elemento fuera pulsable y giró la interfaz a vertical; así el test recorrió las seis pantallas de la tienda con el móvil abierto, en inglés y en español.

El test subía el campo por encima de la barra de la hoja

Vuelvo al test de la matrícula. Con Guardar arreglado, fallaba con «Neither element nor any descendant has keyboard focus». El bucle de scroll había subido el campo de la matrícula hasta y=75, y la barra de la hoja empieza en y=106. Los deslizamientos llevaban el campo más allá de la barra y el toque no llegaba a él; no medí por qué pasa en el Duo y no en el iPhone. Un agente cambió el bucle para que deslice despacio, hacia arriba o hacia abajo, hasta dejar el campo por debajo de la barra, y lo volvió a pasar: funciona en el Duo y en un iPhone 17 Pro.

App Store Connect tiene un hueco propio para el iPhone Duo

Apple pedirá estas capturas a partir de abril de 2027 a toda app compilada con el SDK de iOS 27.1 o posterior. El hueco admite la pantalla exterior a 1398 × 2034 y la interior a 2007 × 2853 en vertical (y sus versiones en horizontal), y las fichas que las incluyen muestran una insignia que indica que la app está hecha para el iPhone Duo. Las pantallas del simulador miden justo eso, así que las capturas en bruto encajan. Un detalle: Apple no acepta capturas con canal alfa y los PNG de la pantalla interior lo tenían. Mi herramienta de subida los rechazó antes de enviarlos; en JPEG pasaron.

Lo que todavía no funciona

  • El primer toque. Con el teclado numérico abierto, el primer toque en «Más detalles» del alta de vehículo no abre la pantalla en el Duo. En el iPhone 17 Pro sí. No he encontrado por qué.
  • La postura de tienda de campaña. App Store Connect solo pide la pantalla exterior y la interior, así que ni la he mirado.
  • El precio del arreglo. toolbarVerticalBehavior(.disabled) renuncia a la barra vertical que Apple diseñó para el móvil cerrado. Los botones han vuelto, pero las hojas no se comportan en el Duo como Apple pensó.
  • El simulador no es el dispositivo. En X, el desarrollador de iOS Ehsan Azish contó que un diseño para el Duo que se veía bien con la release candidate de Xcode salió mal en el dispositivo físico: la barra de pestañas volvía a la de un iPhone normal. Le pregunté si también pasaba en el simulador: «solo en el dispositivo». Hasta que tenga uno en la mano, todo lo de arriba es «funciona en el simulador».

Por dónde empezar

Si compilas tu app con el SDK de iOS 27.1:

  • Pasa los tests de UI de tus hojas por un simulador de Duo nuevo, sin ventana. En un iPhone normal no verás que desaparecen los botones.
  • Si desaparecen, prueba .toolbarVerticalBehavior(.disabled) en el contenido del NavigationStack de la hoja. Recuperas los botones y pierdes la barra que Apple pensó para el móvil cerrado.
  • Deja screenConfig fuera de tus scripts. Cuelga SpringBoard en bucle.
  • Compara el nombre de cada simulador con tus reglas de limpieza. El mío encajaba, y la limpieza de otro agente lo borró.
  • Captura la pantalla interior desde el Mac, no desde XCTest, y exporta en JPEG para App Store Connect.

Fuentes

Lectura relacionada: subagentes o skills en Claude Code y el simulador compartido y el Mac saturado y el informe que falló.


P.D. ¿Has probado ya en un iPhone Duo de verdad? Cuéntame en LinkedIn en qué te engañó el simulador.

Comparte este artículo

Artículos relacionados