On this page
The iPhone Duo is two phones' worth of glass on one hinge. Closed, its outer display is a narrow iPhone. Open, the inner display is close to a small iPad in landscape. Half open, it can stand like a book in your hands or like a laptop on a table.
OdoKeep is built with React Native and Expo, and it already ran on iPhones, iPads and Android tablets. I assumed the Duo would mostly work. It launched, which is not the same thing. This is what it took to make it feel like it belonged on the device, and which of Apple's guidelines decided each change.
What Apple asks for
Apple published a "Designing for iPhone Duo" page in the Human Interface Guidelines alongside the device. The parts that shaped this work are short:
- Design for two size classes: compact width on the outer display, regular width on the inner one. Every pose is one of those two, so an app that adapts to size classes is most of the way there.
- Stay freely resizable. No fixed widths, no hard breakpoints, no metric tied to one particular screen.
- Do not build a separate layout for each pose. Adjust what you already have.
- The fold is a reserved region, like the camera housings. Keep important content out of it while the device is bent.
- Apple's own example is Mail: closed, you see the list or a message; open, you see both side by side.
The last two points sound simple. Getting there needed one patch to a native dependency, one small native module, and a rule about which layouts are allowed to exist.
The Duo says it is an iPad
The first surprise was the tab bar. Open, the Duo reports the iPad idiom with a regular-width window. In React Native, Platform.isPad is true there. UIKit then treats the tab controller as an iPad's, and with the configuration OdoKeep uses on phones it drew no tabs at all, just a floating glyph for quick record that did not respond.
The idiom cannot tell a Duo from an iPad, but the device model can. The app uses react-native-screens for native tabs, and a small patch gives its tab controller a compact horizontal size class on iPhones, through traitOverrides, decided by UIDevice.model. With that override, iOS draws the phone's tab bar as the Duo's side rail, with every tab and a working quick record button. iPads are deliberately left alone: they keep the system's top bar, which the reader can turn into a sidebar.
JavaScript needs the same distinction, so the native module exposes isPhone, and the app's isIPad is Platform.isPad and not a phone. Anything that used to ask "is this an iPad?" now asks the question it actually meant.
A cold launch that dropped the tabs
The override fixed the bar everywhere except one case: launching the app with the Duo already open. The rail appeared with only the prominent tab, the pencil, and none of the others. Rotating the device brought them back.
That was the clue. UIKit builds the platter that holds the ordinary tabs only during a real size class transition. A bar created compact inside a regular-width window never goes through one, so the platter is never built.
The patch now takes the bar through that transition once, the first time it appears in a regular-width window: Regular, a layout pass, then Compact and another layout pass, all synchronously in viewIsAppearing: before the first frame is drawn. I tried the gentler options first. A layout pass alone, reinstalling the tabs, setting the bar's mode explicitly, overriding traits on the window, and splitting the round trip across run loop turns all left the rail with one tab.
The patch is pinned to an exact version of the library, and a test holds the pin, the patch and the installed copy together. A looser version range would let an install update the package and silently drop the fix. The README next to the patch says when to delete it: as soon as the Duo keeps its tabs without it.
Reading the fold
To keep content out of the bend, the app has to know where the bend is. iOS 27.1 reports it as a division reserved region, reservedRegions(kind: .division). It is only active while the inner display is bent. A flat or a closed Duo, every other iPhone and every iPad report nothing.
The region has no notification of its own. What UIKit does do is invalidate the layout of a view that has read it, the same way it does for corner configurations. So the native module installs a transparent view that spans the window, reads the region in layoutSubviews, and reports changes:
func read() {
let next = reservedRegions(kind: .division)
.first(where: \.isActive)?.frame
if hasReported && next == fold { return }
hasReported = true
fold = next
onChange?(next)
}
On the JavaScript side, useFold() turns that rectangle into a posture. A fold taller than it is wide means the device is held like a book; a fold wider than it is tall means tabletop. The band is 40 points, including the system's margins.
I looked at existing hinge packages first. The ones I found report the hinge angle, which is the wrong question. The angle says how far the device is open; the reserved region says where the content must not go, which is the thing a layout needs.
One rule for every layout
The fold could have become an excuse for a set of special screens. Apple asks for the opposite, and so does maintenance. OdoKeep has two screens where a list opens something next to it: the garage, where a vehicle card opens that vehicle's page, and fuel prices, where the station list sits next to the map. Both go through one hook, useSplitLayout, which returns one of three arrangements:
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) };
That excerpt is simplified; the real one also checks that the fold sits inside the window and accounts for the leading safe area.
- Closed, the window is narrow and every screen is the phone's.
- Open and flat, the list keeps a phone-wide leading pane, 40% of the width held between 360 and 440 points, and the detail takes the rest. That is the same rule the iPad uses.
- Book, the list widens to the fold, so the two panes meet on the hinge and nothing sits in the bend.
- Tabletop, fuel prices put the map in the upper half, standing like a screen, and the list in the lower half under your thumbs. The fold's band between them carries nothing. The garage keeps its ordinary list, because a column of cards does not improve by being cut in half.
The flat layout is the one I got wrong first. I built a split centered on the fold, two equal halves, because the hardware is symmetrical. On screen it looked like two phones glued together, and a list of short rows at half of the Duo's width was mostly empty space. Apple's Mail example uses a narrow list and a wide detail, and that is what I went back to. The symmetry belongs to the book pose, where the hinge is physically there in your hands.
Folding must not reset anything
Opening, closing and bending the Duo change the window and the fold, and the garage moves between split and single as they do. The first version of the split garage rendered a different component tree for each, so every fold rebuilt the screen. The cards replayed their entrance animation, the scroll position jumped to the top and a half-typed search disappeared.
The fix is one tree in both states. The list pane is the same component either way, and only its width changes. The detail pane appears beside it when there is room. Each vehicle card keeps its link to the vehicle's own screen. In a split, the press handler prevents that navigation and selects the vehicle in the detail pane instead. On the closed Duo the same card opens the vehicle's screen, as it does on any phone.
Small things on the rail
When the Duo is open, the side rail on the trailing edge also carries the inner camera and the clock. Most of the app was already laid out inside the safe area, but toasts computed their own horizontal insets and ran under the rail. They now add the side insets from the safe area too.
Testing had its own surprise. xcrun simctl io screenshot returns a black image for the Duo simulator, so screenshots for review came from the simulator window itself. The App Store has its own Duo screenshot slot at 2853 by 2007 pixels, the inner display in landscape. OdoKeep's store screenshots are drawn from code rather than captured. The Duo got its own storyboard with the device in each pose, and the drawn screens follow the same layout rule as the app, so the book pose in a screenshot meets on the fold just like the real one.
What is not done
There is no custom layout per pose, by choice. The arrangements are the ones above and nothing else. Screens without a list and a companion view simply get a wider window and adapt the way they already did on an iPad.
The tab bar fix is a patch to a third-party native library. It is pinned, tested and documented, but it remains a patch, and each new release of the library and of iOS needs a check that it is still needed. Fold detection requires iOS 27.1; on earlier versions the Duo still works, it just never reports a bent posture, so the book and tabletop arrangements do not appear.
If I had to keep one lesson, it would be to ask the platform what the space is, not what the device is. The idiom said iPad, the hinge angle said 90 degrees, and neither of those answered the question the layout had. The reserved region and the size class did.
The iPhone Duo support described here is part of the next update of OdoKeep for iOS. Apple's guidance is on the Designing for iPhone Duo page of the Human Interface Guidelines.