In diesem Artikel
Das iPhone Duo ist das Glas von zwei Telefonen an einem Scharnier. Zugeklappt ist sein äußeres Display ein schmales iPhone. Aufgeklappt kommt das innere Display einem kleinen iPad im Querformat nahe. Halb geöffnet lässt es sich wie ein Buch in den Händen halten oder wie ein Laptop auf den Tisch stellen.
OdoKeep ist mit React Native und Expo gebaut und lief bereits auf iPhones, iPads und Android-Tablets. Ich ging davon aus, dass das Duo größtenteils funktionieren würde. Die App startete, und das ist nicht dasselbe. Hier steht, was nötig war, damit sie sich auf dem Gerät zu Hause anfühlt, und welche von Apples Richtlinien die einzelnen Änderungen bestimmt hat.
Was Apple verlangt
Apple hat zusammen mit dem Gerät die Seite „Designing for iPhone Duo“ in den Human Interface Guidelines veröffentlicht. Die Teile, die diese Arbeit geprägt haben, sind kurz:
- Für zwei Größenklassen gestalten: kompakte Breite auf dem äußeren Display, reguläre Breite auf dem inneren. Jede Haltung entspricht einer dieser beiden, eine App, die sich an Größenklassen anpasst, ist also schon fast am Ziel.
- Frei skalierbar bleiben. Keine festen Breiten, keine harten Umbruchpunkte, keine Maße, die an einen bestimmten Bildschirm gebunden sind.
- Kein eigenes Layout für jede Haltung bauen. Das vorhandene anpassen.
- Die Faltkante ist ein reservierter Bereich, wie die Kameraaussparungen. Wichtige Inhalte gehören nicht hinein, solange das Gerät angewinkelt ist.
- Apples eigenes Beispiel ist Mail: Zugeklappt siehst du die Liste oder eine Nachricht, aufgeklappt beides nebeneinander.
Die letzten beiden Punkte klingen einfach. Um dorthin zu kommen, brauchte es einen Patch für eine native Abhängigkeit, ein kleines natives Modul und eine Regel dafür, welche Layouts überhaupt existieren dürfen.
Das Duo gibt sich als iPad aus
Die erste Überraschung war die Tab-Leiste. Aufgeklappt meldet das Duo das iPad-Idiom mit einem Fenster in regulärer Breite. In React Native ist Platform.isPad dort true. UIKit behandelt den Tab-Controller dann wie den eines iPads, und mit der Konfiguration, die OdoKeep auf Telefonen verwendet, zeichnete es gar keine Tabs, nur ein schwebendes Symbol für „Schnell erfassen“, das nicht reagierte.
Das Idiom kann ein Duo nicht von einem iPad unterscheiden, das Gerätemodell schon. Die App verwendet react-native-screens für native Tabs, und ein kleiner Patch gibt dessen Tab-Controller auf iPhones über traitOverrides eine kompakte horizontale Größenklasse, entschieden anhand von UIDevice.model. Mit diesem Override zeichnet iOS die Tab-Leiste des Telefons als seitliche Leiste des Duo, mit allen Tabs und einer funktionierenden Taste für „Schnell erfassen“. iPads bleiben bewusst unangetastet: Sie behalten die obere Leiste des Systems, die du in eine Seitenleiste umwandeln kannst.
JavaScript braucht dieselbe Unterscheidung, deshalb stellt das native Modul isPhone bereit, und isIPad bedeutet in der App Platform.isPad und kein Telefon. Alles, was früher fragte „Ist das ein iPad?“, stellt jetzt die Frage, die eigentlich gemeint war.
Ein Kaltstart, der die Tabs verlor
Der Override reparierte die Leiste überall außer in einem Fall: beim Start der App mit bereits aufgeklapptem Duo. Die seitliche Leiste erschien nur mit dem hervorgehobenen Tab, dem Stift, und ohne alle anderen. Eine Drehung des Geräts brachte sie zurück.
Das war der Hinweis. UIKit baut den Platter, der die gewöhnlichen Tabs enthält, nur während eines echten Wechsels der Größenklasse. Eine Leiste, die kompakt in einem Fenster regulärer Breite entsteht, durchläuft nie einen solchen Wechsel, also wird der Platter nie gebaut.
Der Patch schickt die Leiste deshalb einmal durch diesen Wechsel, wenn sie zum ersten Mal in einem Fenster regulärer Breite erscheint: Regular, ein Layoutdurchlauf, dann Compact und ein weiterer Layoutdurchlauf, alles synchron in viewIsAppearing:, bevor der erste Frame gezeichnet wird. Zuerst habe ich die sanfteren Varianten probiert. Ein Layoutdurchlauf allein, die Tabs neu installieren, den Modus der Leiste explizit setzen, Traits am Fenster überschreiben und den Hin- und Rückweg auf mehrere Durchläufe der Run Loop verteilen: Bei allen blieb die seitliche Leiste mit einem einzigen Tab.
Der Patch ist auf eine exakte Version der Bibliothek festgelegt, und ein Test hält Festlegung, Patch und installierte Kopie zusammen. Ein lockerer Versionsbereich würde einer Installation erlauben, das Paket zu aktualisieren und den Fix stillschweigend zu verlieren. Die README neben dem Patch sagt, wann er gelöscht werden kann: sobald das Duo seine Tabs auch ohne ihn behält.
Die Faltkante lesen
Um Inhalte aus dem Knick herauszuhalten, muss die App wissen, wo der Knick ist. iOS 27.1 meldet ihn als reservierten Bereich der Art Division, reservedRegions(kind: .division). Er ist nur aktiv, solange das innere Display angewinkelt ist. Ein flaches oder zugeklapptes Duo, jedes andere iPhone und jedes iPad melden nichts.
Der Bereich hat keine eigene Benachrichtigung. UIKit invalidiert aber das Layout einer View, die ihn gelesen hat, genauso wie bei Eckenkonfigurationen. Das native Modul installiert deshalb eine transparente View über das ganze Fenster, liest den Bereich in layoutSubviews und meldet Änderungen:
func read() {
let next = reservedRegions(kind: .division)
.first(where: \.isActive)?.frame
if hasReported && next == fold { return }
hasReported = true
fold = next
onChange?(next)
}
Auf der JavaScript-Seite macht useFold() aus diesem Rechteck eine Haltung. Eine Faltkante, die höher als breit ist, bedeutet, dass das Gerät wie ein Buch gehalten wird; eine, die breiter als hoch ist, bedeutet Tischstellung. Der Streifen misst 40 Punkte, die Ränder des Systems eingeschlossen.
Zuerst habe ich mir vorhandene Scharnier-Pakete angesehen. Die, die ich gefunden habe, melden den Scharnierwinkel, und das ist die falsche Frage. Der Winkel sagt, wie weit das Gerät geöffnet ist; der reservierte Bereich sagt, wohin der Inhalt nicht darf, und genau das braucht ein Layout.
Eine Regel für jedes Layout
Die Faltkante hätte zum Vorwand für eine Reihe von Spezialbildschirmen werden können. Apple verlangt das Gegenteil, und die Wartbarkeit auch. OdoKeep hat zwei Bildschirme, auf denen eine Liste etwas neben sich öffnet: die Garage, in der eine Fahrzeugkarte die Seite dieses Fahrzeugs öffnet, und die Kraftstoffpreise, bei denen die Liste der Tankstellen neben der Karte liegt. Beide laufen über einen Hook, useSplitLayout, der eine von drei Anordnungen zurückgibt:
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) };
Der Ausschnitt ist vereinfacht; der echte prüft außerdem, ob die Faltkante innerhalb des Fensters liegt, und berücksichtigt den sicheren Bereich auf der führenden Seite.
- Zugeklappt ist das Fenster schmal, und jeder Bildschirm ist der des Telefons.
- Aufgeklappt und flach behält die Liste einen telefonbreiten Bereich auf der führenden Seite, 40 % der Breite, gehalten zwischen 360 und 440 Punkten, und die Detailansicht bekommt den Rest. Das ist dieselbe Regel, die auch das iPad verwendet.
- In Buchstellung wird die Liste bis zur Faltkante breiter, sodass sich die beiden Bereiche am Scharnier treffen und nichts im Knick liegt.
- In Tischstellung legen die Kraftstoffpreise die Karte in die obere Hälfte, aufgestellt wie ein Bildschirm, und die Liste in die untere Hälfte unter deine Daumen. Der Streifen der Faltkante dazwischen trägt nichts. Die Garage behält ihre normale Liste, denn eine Spalte aus Fahrzeugkarten wird nicht besser, wenn man sie halbiert.
Beim flachen Layout lag ich zuerst falsch. Ich baute eine Teilung, die auf der Faltkante zentriert war, zwei gleiche Hälften, weil die Hardware symmetrisch ist. Auf dem Bildschirm sah das aus wie zwei zusammengeklebte Telefone, und eine Liste kurzer Zeilen auf halber Breite des Duo war größtenteils leerer Raum. Apples Mail-Beispiel verwendet eine schmale Liste und eine breite Detailansicht, und dazu bin ich zurückgekehrt. Die Symmetrie gehört zur Buchstellung, in der das Scharnier tatsächlich in deinen Händen liegt.
Falten darf nichts zurücksetzen
Aufklappen, Zuklappen und Anwinkeln des Duo verändern Fenster und Faltkante, und die Garage wechselt dabei zwischen geteilter und einfacher Ansicht. Die erste Version der geteilten Garage renderte für jeden Zustand einen anderen Komponentenbaum, also baute jedes Falten den Bildschirm neu auf. Die Karten spielten ihre Einblendanimation erneut ab, die Scrollposition sprang nach oben, und eine halb eingetippte Suche verschwand.
Die Lösung ist ein einziger Baum für beide Zustände. Der Listenbereich ist in beiden Fällen dieselbe Komponente, nur seine Breite ändert sich. Der Detailbereich erscheint daneben, wenn Platz ist. Jede Fahrzeugkarte behält ihren Link auf den eigenen Bildschirm des Fahrzeugs. In der geteilten Ansicht verhindert der Press-Handler diese Navigation und wählt das Fahrzeug stattdessen im Detailbereich aus. Auf dem zugeklappten Duo öffnet dieselbe Karte den Bildschirm des Fahrzeugs, wie auf jedem Telefon.
Kleinigkeiten an der seitlichen Leiste
Wenn das Duo aufgeklappt ist, trägt die seitliche Leiste an der nachfolgenden Kante auch die innere Kamera und die Uhr. Der größte Teil der App war bereits innerhalb des sicheren Bereichs angeordnet, aber Toasts berechneten ihre horizontalen Abstände selbst und liefen unter die Leiste. Sie addieren jetzt auch die seitlichen Abstände des sicheren Bereichs.
Auch das Testen hatte seine Überraschung. xcrun simctl io screenshot liefert für den Duo-Simulator ein schwarzes Bild, deshalb stammten die Screenshots für die Durchsicht aus dem Simulatorfenster selbst. Der App Store hat einen eigenen Screenshot-Platz für das Duo mit 2853 mal 2007 Pixeln, das innere Display im Querformat. Die Store-Screenshots von OdoKeep werden aus Code gezeichnet, nicht aufgenommen. Das Duo bekam ein eigenes Storyboard mit dem Gerät in jeder Haltung, und die gezeichneten Bildschirme folgen derselben Layoutregel wie die App, sodass sich die beiden Bereiche der Buchstellung in einem Screenshot genau wie in der echten an der Faltkante treffen.
Was nicht umgesetzt ist
Es gibt bewusst kein eigenes Layout pro Haltung. Die Anordnungen sind die oben genannten und keine anderen. Bildschirme ohne Liste und Begleitansicht bekommen einfach ein breiteres Fenster und passen sich so an, wie sie es schon auf dem iPad taten.
Der Fix für die Tab-Leiste ist ein Patch an einer nativen Bibliothek eines Drittanbieters. Er ist festgelegt, getestet und dokumentiert, bleibt aber ein Patch, und bei jeder neuen Version der Bibliothek und von iOS muss geprüft werden, ob er noch nötig ist. Die Erkennung der Faltkante setzt iOS 27.1 voraus; auf älteren Versionen funktioniert das Duo weiterhin, es meldet nur nie eine angewinkelte Haltung, sodass Buch- und Tischstellung nicht erscheinen.
Wenn ich nur eine Lehre behalten dürfte, wäre es diese: die Plattform fragen, was der Raum ist, nicht, was das Gerät ist. Das Idiom sagte iPad, der Scharnierwinkel sagte 90 Grad, und keines davon beantwortete die Frage, die das Layout hatte. Der reservierte Bereich und die Größenklasse taten es.
Die hier beschriebene Unterstützung für das iPhone Duo ist Teil des nächsten Updates von OdoKeep für iOS. Apples Empfehlungen stehen auf der Seite Designing for iPhone Duo der Human Interface Guidelines.