En este artículo
El iPhone Duo es el cristal de dos celulares sobre una sola bisagra. Cerrado, su pantalla exterior es un iPhone angosto. Abierto, la pantalla interior se parece bastante a un iPad pequeño en horizontal. Medio abierto, se puede sostener con las manos como un libro o dejar parado sobre una mesa como una laptop.
OdoKeep está hecho con React Native y Expo, y ya funcionaba en iPhone, iPad y tablets Android. Supuse que el Duo funcionaría casi sin cambios. La aplicación abría, que no es lo mismo. Esto es lo que hizo falta para que se sintiera hecha para el dispositivo, y qué directriz de Apple decidió cada cambio.
Lo que pide Apple
Junto con el dispositivo, Apple publicó la página «Designing for iPhone Duo» en las Human Interface Guidelines. Las partes que le dieron 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 en particular.
- 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í requirió 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 fue 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 celulares, 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 le 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 celular 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 celular. 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 fue 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. Primero probé 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 leyó, 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)
}
Del 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 revisé los paquetes de bisagra que ya existían. Los que encontré informan del ángulo de la bisagra, que es la pregunta equivocada. El ángulo dice qué tan abierto está 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 pudo 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 toma en cuenta el área segura del lado inicial.
- Cerrado, la ventana es angosta y todas las pantallas son las del celular.
- Abierto y plano, la lista conserva un panel inicial con el ancho de un celular, 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, parado 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 a la mitad.
La disposición plana fue 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 celulares 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 angosta y un detalle ancho, y a eso regresé. 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 hasta arriba 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 celular.
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 tuvieron su propia sorpresa. xcrun simctl io screenshot devuelve una imagen negra en el simulador del Duo, así que las capturas para revisión salieron 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 recibió 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 preguntarle 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.