Adaptar React Native al iPhone Duo: leer el pliegue, no el dispositivo

Arreglar una barra de pestañas que desaparecía, leer el pliegue como región reservada de UIKit y mantener una sola regla de diseño en todas las posturas del iPhone Duo.

Actualizado el

Todos los artículos
En este artículo

El iPhone Duo es el cristal de dos móviles sobre una sola bisagra. Cerrado, su pantalla exterior es un iPhone estrecho. Abierto, la pantalla interior se parece bastante a un iPad pequeño en horizontal. Medio abierto, puede sostenerse entre las manos como un libro o quedarse de pie sobre una mesa como un portátil.

OdoKeep está hecho con React Native y Expo, y ya funcionaba en iPhone, iPad y tabletas Android. Suponía que el Duo funcionaría casi sin cambios. La aplicación se abría, que no es lo mismo. Esto es lo que ha hecho falta para que pareciera hecha para el dispositivo, y qué directriz de Apple ha decidido cada cambio.

Lo que pide Apple

Junto con el dispositivo, Apple ha publicado la página «Designing for iPhone Duo» en las Human Interface Guidelines. Las partes que han dado forma a este trabajo son breves:

  • Diseñar para dos clases de tamaño: ancho compacto en la pantalla exterior y ancho regular en la interior. Cada postura es una de esas dos, así que una aplicación que ya se adapta a las clases de tamaño tiene casi todo el camino hecho.
  • Seguir siendo libremente redimensionable. Nada de anchos fijos, puntos de corte rígidos ni medidas atadas a una pantalla concreta.
  • No construir un diseño distinto para cada postura. Ajustar el que ya existe.
  • El pliegue es una región reservada, como los alojamientos de las cámaras. El contenido importante debe quedar fuera de él mientras el dispositivo está doblado.
  • El ejemplo de la propia Apple es Mail: cerrado, se ve la lista o un mensaje; abierto, ambos uno al lado del otro.

Los dos últimos puntos parecen sencillos. Llegar ahí ha requerido un parche en una dependencia nativa, un pequeño módulo nativo y una regla sobre qué diseños pueden existir.

El Duo dice que es un iPad

La primera sorpresa ha sido la barra de pestañas. Abierto, el Duo se identifica con el idiom de iPad y una ventana de ancho regular. En React Native, Platform.isPad es verdadero en ese caso. UIKit trata entonces el controlador de pestañas como el de un iPad y, con la configuración que OdoKeep usa en los móviles, no dibujaba ninguna pestaña: solo un glifo flotante de registro rápido que no respondía.

El idiom no distingue un Duo de un iPad, pero el modelo del dispositivo sí. La aplicación usa react-native-screens para las pestañas nativas, y un pequeño parche da a su controlador de pestañas una clase de tamaño horizontal compacta en los iPhone, mediante traitOverrides, según UIDevice.model. Con esa sustitución, iOS dibuja la barra de pestañas del móvil como el riel lateral del Duo, con todas las pestañas y un botón de registro rápido que funciona. Los iPad se dejan como están a propósito: conservan la barra superior del sistema, que el usuario puede convertir en una barra lateral.

JavaScript necesita la misma distinción, así que el módulo nativo expone isPhone, y el isIPad de la aplicación es Platform.isPad siempre que no se trate de un móvil. Todo lo que antes preguntaba «¿es esto un iPad?» ahora hace la pregunta que realmente quería hacer.

Un arranque en frío que perdía las pestañas

La sustitución arreglaba la barra en todos los casos menos uno: abrir la aplicación con el Duo ya desplegado. El riel aparecía solo con la pestaña destacada, la del lápiz, y sin ninguna de las demás. Al girar el dispositivo, volvían.

Esa era la pista. UIKit construye la bandeja que contiene las pestañas normales solo durante una transición real de clase de tamaño. Una barra creada como compacta dentro de una ventana de ancho regular nunca pasa por ninguna, así que la bandeja nunca se construye.

Ahora el parche hace pasar la barra por esa transición una vez, la primera vez que aparece en una ventana de ancho regular: Regular, una pasada de layout, luego Compact y otra pasada de layout, todo de forma síncrona en viewIsAppearing: antes de dibujar el primer fotograma. Antes he probado las opciones más suaves. Una pasada de layout sola, reinstalar las pestañas, fijar explícitamente el modo de la barra, sustituir los traits en la ventana y repartir el viaje de ida y vuelta entre varias vueltas del run loop: todas dejaban el riel con una sola pestaña.

El parche está fijado a una versión exacta de la biblioteca, y una prueba mantiene sincronizados la versión fijada, el parche y la copia instalada. Un rango de versiones más amplio permitiría que una instalación actualizara el paquete y perdiera el arreglo sin avisar. El README que acompaña al parche indica cuándo eliminarlo: en cuanto el Duo conserve sus pestañas sin él.

Leer el pliegue

Para mantener el contenido fuera de la curvatura, la aplicación tiene que saber dónde está. iOS 27.1 la comunica como una región reservada de división, reservedRegions(kind: .division). Solo está activa mientras la pantalla interior está doblada. Un Duo plano o cerrado, cualquier otro iPhone y cualquier iPad no comunican nada.

La región no tiene notificación propia. Lo que sí hace UIKit es invalidar el layout de una vista que la ha leído, igual que con las configuraciones de esquinas. Así que el módulo nativo instala una vista transparente que ocupa toda la ventana, lee la región en layoutSubviews y comunica los cambios:

func read() {
  let next = reservedRegions(kind: .division)
    .first(where: \.isActive)?.frame
  if hasReported && next == fold { return }
  hasReported = true
  fold = next
  onChange?(next)
}

En el lado de JavaScript, useFold() convierte ese rectángulo en una postura. Un pliegue más alto que ancho significa que el dispositivo se sostiene como un libro; uno más ancho que alto significa postura de mesa. La franja mide 40 puntos, incluidos los márgenes del sistema.

Primero he revisado los paquetes de bisagra que ya existían. Los que he encontrado informan del ángulo de la bisagra, que es la pregunta equivocada. El ángulo dice cuánto está abierto el dispositivo; la región reservada dice dónde no debe ir el contenido, que es lo que necesita un diseño.

Una regla para todos los diseños

El pliegue podría haberse convertido en la excusa para un conjunto de pantallas especiales. Apple pide lo contrario, y el mantenimiento también. OdoKeep tiene dos pantallas en las que una lista abre algo a su lado: el garaje, donde la tarjeta de un vehículo abre la página de ese vehículo, y los precios del combustible, donde la lista de gasolineras está junto al mapa. Ambas pasan por un único hook, useSplitLayout, que devuelve una de tres disposiciones:

if (fold?.posture === "tabletop") {
  return { kind: "stacked", topHeight: fold.fold.y, gap: fold.fold.height };
}
if (layoutWidth < SPLIT_MIN_WIDTH) return { kind: "single" };
if (fold?.posture === "book") {
  return { kind: "columns", listWidth: fold.fold.x + fold.fold.width / 2 };
}
return { kind: "columns", listWidth: splitListPaneWidth(layoutWidth) };

Ese extracto está simplificado; el real también comprueba que el pliegue quede dentro de la ventana y tiene en cuenta el área segura del lado inicial.

  • Cerrado, la ventana es estrecha y todas las pantallas son las del móvil.
  • Abierto y plano, la lista conserva un panel inicial con el ancho de un móvil, el 40 % del ancho limitado entre 360 y 440 puntos, y el detalle ocupa el resto. Es la misma regla que usa el iPad.
  • Libro, la lista se ensancha hasta el pliegue, de modo que los dos paneles se encuentran en la bisagra y nada queda en la curvatura.
  • Mesa, los precios del combustible colocan el mapa en la mitad superior, de pie como una pantalla, y la lista en la mitad inferior, al alcance de los pulgares. La franja del pliegue entre ambos no lleva nada. El garaje mantiene su lista normal, porque una columna de tarjetas no mejora al partirla por la mitad.

La disposición plana es la que primero hice mal. Construí una división centrada en el pliegue, con dos mitades iguales, porque el hardware es simétrico. En pantalla parecían dos móviles pegados, y una lista de filas cortas en la mitad del ancho del Duo era casi todo espacio vacío. El ejemplo de Mail de Apple usa una lista estrecha y un detalle ancho, y a eso he vuelto. La simetría corresponde a la postura de libro, donde la bisagra está físicamente ahí, entre las manos.

Plegar no debe reiniciar nada

Abrir, cerrar y doblar el Duo cambia la ventana y el pliegue, y el garaje pasa de la vista dividida a la vista única según cambian. La primera versión del garaje dividido renderizaba un árbol de componentes distinto para cada caso, así que cada pliegue reconstruía la pantalla. Las tarjetas repetían su animación de entrada, la posición de desplazamiento saltaba arriba del todo y una búsqueda a medio escribir desaparecía.

La solución es un solo árbol en ambos estados. El panel de la lista es el mismo componente en los dos casos, y solo cambia su ancho. El panel de detalle aparece a su lado cuando hay espacio. Cada tarjeta de vehículo conserva su enlace a la pantalla propia del vehículo. En la vista dividida, el manejador de pulsación impide esa navegación y, en su lugar, selecciona el vehículo en el panel de detalle. En el Duo cerrado, la misma tarjeta abre la pantalla del vehículo, como en cualquier móvil.

Detalles pequeños en el riel

Con el Duo abierto, el riel lateral del borde final también aloja la cámara interior y el reloj. Casi toda la aplicación ya se distribuía dentro del área segura, pero los toasts calculaban sus propios márgenes horizontales y quedaban debajo del riel. Ahora también suman los márgenes laterales del área segura.

Las pruebas han tenido su propia sorpresa. xcrun simctl io screenshot devuelve una imagen negra en el simulador del Duo, así que las capturas para revisión se han sacado de la propia ventana del simulador. El App Store tiene un espacio de capturas propio para el Duo, de 2853 por 2007 píxeles: la pantalla interior en horizontal. Las capturas de OdoKeep para la tienda se dibujan desde código en lugar de capturarse. El Duo ha recibido su propio storyboard con el dispositivo en cada postura, y las pantallas dibujadas siguen la misma regla de diseño que la aplicación, de modo que en una captura la postura de libro se encuentra en el pliegue igual que la real.

Lo que no está hecho

No hay un diseño personalizado por postura, por decisión propia. Las disposiciones son las de arriba y ninguna más. Las pantallas que no tienen una lista con una vista complementaria simplemente reciben una ventana más ancha y se adaptan como ya lo hacían en un iPad.

El arreglo de la barra de pestañas es un parche a una biblioteca nativa de terceros. Está fijado, probado y documentado, pero sigue siendo un parche, y cada nueva versión de la biblioteca y de iOS exige comprobar si sigue haciendo falta. La detección del pliegue requiere iOS 27.1; en versiones anteriores el Duo sigue funcionando, solo que nunca comunica una postura doblada, así que las disposiciones de libro y de mesa no aparecen.

Si tuviera que quedarme con una sola lección, sería preguntar a la plataforma qué espacio hay, no qué dispositivo es. El idiom decía iPad, el ángulo de la bisagra decía 90 grados, y ninguno de los dos respondía a la pregunta que tenía el diseño. La región reservada y la clase de tamaño, sí.

La compatibilidad con el iPhone Duo descrita aquí forma parte de la próxima actualización de OdoKeep para iOS. La guía de Apple está en la página Designing for iPhone Duo de las Human Interface Guidelines.

Empieza el diario en tu próximo repostaje.

OdoKeep ya está en el App Store. Empieza gratis, privado para siempre. Android está en camino.

Código QR para descargar OdoKeep en el App Store

Apunta con la cámara del móvil