Supportare l’iPhone Duo in React Native: leggere la piega, non il dispositivo

Riparare una barra dei pannelli scomparsa, leggere la piega come regione riservata di UIKit e mantenere una sola regola di layout in ogni postura dell’iPhone Duo.

Aggiornato il

Tutti gli articoli
In questo articolo

L’iPhone Duo mette la superficie di due telefoni su un’unica cerniera. Chiuso, lo schermo esterno è un iPhone stretto. Aperto, lo schermo interno si avvicina a un piccolo iPad in orizzontale. Aperto a metà, si può tenere in mano come un libro o appoggiare sul tavolo come un portatile.

OdoKeep è costruita con React Native ed Expo e girava già su iPhone, iPad e tablet Android. Pensavo che il Duo avrebbe funzionato quasi da solo. L’app si avviava, che non è la stessa cosa. Questo è ciò che è servito perché sembrasse fatta per il dispositivo, e quale indicazione di Apple ha deciso ogni modifica.

Cosa chiede Apple

Insieme al dispositivo, Apple ha pubblicato nelle Human Interface Guidelines una pagina «Designing for iPhone Duo». Le parti che hanno guidato questo lavoro sono brevi:

  • Progettare per due classi di dimensione: larghezza compatta sullo schermo esterno, larghezza regolare su quello interno. Ogni postura è una delle due, quindi un’app che si adatta alle classi di dimensione ha già fatto quasi tutto.
  • Restare liberamente ridimensionabili. Niente larghezze fisse, niente breakpoint rigidi, nessuna misura legata a uno schermo in particolare.
  • Non costruire un layout separato per ogni postura. Adattare quello che c’è già.
  • La piega è una regione riservata, come gli alloggiamenti delle fotocamere. I contenuti importanti devono starne fuori finché il dispositivo è piegato.
  • L’esempio di Apple è Mail: da chiuso si vede l’elenco o un messaggio; da aperto si vedono entrambi, uno accanto all’altro.

Gli ultimi due punti sembrano semplici. Per arrivarci sono serviti una patch a una dipendenza nativa, un piccolo modulo nativo e una regola su quali layout possono esistere.

Il Duo dice di essere un iPad

La prima sorpresa è stata la barra dei pannelli. Da aperto, il Duo dichiara l’idioma iPad con una finestra di larghezza regolare. In React Native, lì Platform.isPad è true. UIKit tratta quindi il controller dei pannelli come quello di un iPad e, con la configurazione che OdoKeep usa sui telefoni, non disegnava nessun pannello: solo un glifo fluttuante per la registrazione rapida, che non rispondeva.

L’idioma non distingue un Duo da un iPad, il modello del dispositivo sì. L’app usa react-native-screens per i pannelli nativi e una piccola patch dà al suo controller dei pannelli una classe di dimensione orizzontale compatta sugli iPhone, tramite traitOverrides, in base a UIDevice.model. Con questa sostituzione, iOS disegna la barra dei pannelli del telefono come la barra laterale del Duo, con tutti i pannelli e un pulsante di registrazione rapida funzionante. Gli iPad restano volutamente esclusi: mantengono la barra superiore del sistema, che chi legge può trasformare in una barra laterale.

Anche JavaScript ha bisogno della stessa distinzione, quindi il modulo nativo espone isPhone e l’isIPad dell’app è Platform.isPad purché il dispositivo non sia un telefono. Tutto ciò che prima chiedeva «è un iPad?» ora pone la domanda che intendeva davvero.

Un avvio a freddo che perdeva i pannelli

La sostituzione ha sistemato la barra ovunque tranne in un caso: avviare l’app con il Duo già aperto. La barra laterale compariva solo con il pannello in evidenza, la matita, e senza nessuno degli altri. Ruotando il dispositivo tornavano.

Quello era l’indizio. UIKit costruisce il contenitore che ospita i pannelli ordinari solo durante una vera transizione di classe di dimensione. Una barra creata compatta dentro una finestra di larghezza regolare non ne attraversa mai una, quindi quel contenitore non viene mai costruito.

Ora la patch fa passare la barra per quella transizione una volta, la prima volta che compare in una finestra di larghezza regolare: Regular, un passaggio di layout, poi Compact e un altro passaggio di layout, tutto in modo sincrono in viewIsAppearing: prima che venga disegnato il primo fotogramma. Ho provato prima le opzioni più delicate. Un solo passaggio di layout, reinstallare i pannelli, impostare in modo esplicito la modalità della barra, sostituire i tratti sulla finestra e dividere l’andata e ritorno su più giri del run loop lasciavano tutti la barra laterale con un solo pannello.

La patch è fissata a una versione esatta della libreria, e un test tiene allineati il vincolo di versione, la patch e la copia installata. Un intervallo di versioni più ampio permetterebbe a un’installazione di aggiornare il pacchetto e far sparire la correzione senza avvisare. Il README accanto alla patch dice quando eliminarla: appena il Duo mantiene i suoi pannelli senza di essa.

Leggere la piega

Per tenere i contenuti fuori dalla curvatura, l’app deve sapere dove si trova. iOS 27.1 la segnala come regione riservata di divisione, reservedRegions(kind: .division). È attiva solo mentre lo schermo interno è piegato. Un Duo aperto in piano o chiuso, tutti gli altri iPhone e tutti gli iPad non segnalano nulla.

La regione non ha una notifica propria. Quello che UIKit fa è invalidare il layout di una vista che l’ha letta, come avviene per le configurazioni degli angoli. Il modulo nativo installa quindi una vista trasparente che copre la finestra, legge la regione in layoutSubviews e segnala le modifiche:

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

Sul lato JavaScript, useFold() trasforma quel rettangolo in una postura. Una piega più alta che larga significa che il dispositivo è tenuto come un libro; una piega più larga che alta significa postura da tavolo. La fascia è di 40 punti, compresi i margini del sistema.

Prima ho guardato i pacchetti esistenti per le cerniere. Quelli che ho trovato riportano l’angolo della cerniera, che è la domanda sbagliata. L’angolo dice quanto è aperto il dispositivo; la regione riservata dice dove i contenuti non devono andare, ed è questo che serve a un layout.

Una regola per ogni layout

La piega poteva diventare un pretesto per una serie di schermate speciali. Apple chiede il contrario, e lo chiede anche la manutenzione. OdoKeep ha due schermate in cui un elenco apre qualcosa accanto a sé: il garage, dove la scheda di un veicolo apre la pagina di quel veicolo, e i prezzi dei carburanti, dove l’elenco dei distributori sta accanto alla mappa. Entrambe passano da un unico hook, useSplitLayout, che restituisce una di tre disposizioni:

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) };

L’estratto è semplificato; quello reale controlla anche che la piega stia dentro la finestra e tiene conto della safe area sul lato iniziale.

  • Chiuso, la finestra è stretta e ogni schermata è quella del telefono.
  • Aperto e in piano, l’elenco mantiene sul lato iniziale un riquadro largo quanto un telefono, il 40% della larghezza contenuto tra 360 e 440 punti, e il dettaglio occupa il resto. È la stessa regola che usa l’iPad.
  • A libro, l’elenco si allarga fino alla piega, così i due riquadri si incontrano sulla cerniera e nulla resta nella curvatura.
  • Da tavolo, i prezzi dei carburanti mettono la mappa nella metà superiore, in piedi come uno schermo, e l’elenco nella metà inferiore, sotto i pollici. La fascia della piega tra le due non contiene nulla. Il garage mantiene il suo elenco normale, perché una colonna di schede non migliora tagliandola a metà.

Il layout in piano è quello che ho sbagliato per primo. Avevo costruito una divisione centrata sulla piega, due metà uguali, perché l’hardware è simmetrico. Sullo schermo sembravano due telefoni incollati, e un elenco di righe brevi su metà della larghezza del Duo era soprattutto spazio vuoto. L’esempio di Mail di Apple usa un elenco stretto e un dettaglio largo, ed è lì che sono tornato. La simmetria appartiene alla postura a libro, dove la cerniera è fisicamente lì, tra le mani.

Piegare non deve azzerare nulla

Aprire, chiudere e piegare il Duo cambiano la finestra e la piega, e il garage passa di conseguenza dalla vista divisa a quella singola. La prima versione del garage diviso mostrava un albero di componenti diverso per ciascun caso, quindi ogni piegatura ricostruiva la schermata. Le schede ripetevano l’animazione di entrata, la posizione di scorrimento tornava in cima e una ricerca scritta a metà spariva.

La soluzione è un solo albero in entrambi gli stati. Il riquadro dell’elenco è lo stesso componente in tutti e due i casi e cambia solo la sua larghezza. Il riquadro del dettaglio compare accanto quando c’è spazio. Ogni scheda veicolo mantiene il suo link alla schermata del veicolo. Nella vista divisa, il gestore del tocco impedisce quella navigazione e seleziona invece il veicolo nel riquadro del dettaglio. Sul Duo chiuso la stessa scheda apre la schermata del veicolo, come su qualsiasi telefono.

Piccole cose sulla barra laterale

Quando il Duo è aperto, la barra laterale sul lato finale ospita anche la fotocamera interna e l’orologio. Quasi tutta l’app era già disposta dentro la safe area, ma i toast calcolavano i propri margini orizzontali e finivano sotto la barra. Ora aggiungono anche loro i margini laterali della safe area.

Anche i test hanno avuto la loro sorpresa. xcrun simctl io screenshot restituisce un’immagine nera per il simulatore del Duo, quindi gli screenshot per la revisione sono stati presi dalla finestra del simulatore stessa. L’App Store ha uno spazio dedicato agli screenshot del Duo, a 2853 per 2007 pixel: lo schermo interno in orizzontale. Gli screenshot di OdoKeep per lo store sono disegnati dal codice invece che catturati. Il Duo ha avuto il suo storyboard con il dispositivo in ogni postura, e le schermate disegnate seguono la stessa regola di layout dell’app, così nella postura a libro i riquadri di uno screenshot si incontrano sulla piega proprio come sul dispositivo reale.

Cosa non è stato fatto

Non c’è un layout personalizzato per ogni postura, per scelta. Le disposizioni sono quelle descritte sopra e nient’altro. Le schermate senza un elenco e una vista abbinata ricevono semplicemente una finestra più larga e si adattano come facevano già su iPad.

La correzione della barra dei pannelli è una patch a una libreria nativa di terze parti. È fissata a una versione, testata e documentata, ma resta una patch, e a ogni nuova versione della libreria e di iOS bisogna verificare se serve ancora. Il rilevamento della piega richiede iOS 27.1; nelle versioni precedenti il Duo funziona comunque, solo che non segnala mai una postura piegata, quindi le disposizioni a libro e da tavolo non compaiono.

Se dovessi tenere una sola lezione, sarebbe chiedere alla piattaforma che cos’è lo spazio, non che cos’è il dispositivo. L’idioma diceva iPad, l’angolo della cerniera diceva 90 gradi, e nessuno dei due rispondeva alla domanda che aveva il layout. La regione riservata e la classe di dimensione sì.

Il supporto per iPhone Duo descritto qui fa parte del prossimo aggiornamento di OdoKeep per iOS. Le indicazioni di Apple si trovano nella pagina Designing for iPhone Duo delle Human Interface Guidelines.

Inizia il libro di bordo al prossimo rifornimento.

OdoKeep è su App Store. Inizia gratis, privato per sempre. Android sta arrivando.

Codice QR per scaricare OdoKeep su App Store

Inquadra con la fotocamera del telefono