Hello everyone.
The iPhone Duo is not an iPad with a hinge. Both panels report the phone idiom. Both are Regular width. The system reserves an 84-point strip on one physical side for the clock, Wi-Fi and battery — and the top safe-area inset can be zero. Three assumptions that every iPhone layout makes are false at once.
Chute iOS 1.3.0 ships support for the device. These are the assumptions we had to unlearn.
The Device Is Not an iPad
The measurements first, because they explain everything else:
| Panel | Size | Safe area |
|---|---|---|
| Outer (closed) | 466 × 678 pt | {0, 0, 34, 84} |
| Inner (open) | 951 × 669 pt | {0, 0, 34, 84} |
| Inner, split-view pane | 469 × 669 pt | view {82, 84, 34, 0}, window {0, 0, 34, 0} |
Two things stand out. The inner screen is wider than an iPad mini, and the app still gets the phone idiom. And the top inset is zero on the outer panel: the system’s status information lives in the vertical strip, not in a top bar, and the status bar frame degenerates to a two-point rectangle there. Any layout that assumes a 20-, 44- or 59-point top inset will draw under the system’s own clock.
The old code was full of screen-size and top-bar macros, most of them read once at startup. On a device that changes its scene size at runtime, those are stale the moment the user folds or unfolds. A foldable is a phone that changes its mind about its size while you are using it.
The replacements are boring and correct: the view’s own bounds, its safe-area insets, and a single helper for “is this a wide layout?” that does not ask whether the device is an iPad. Asking whether the device is an iPad answers no on a 951-point screen; the idiom agrees. Width is a property of the window, not of the product name.
The Strip Is Physical
HIG’s guidance for iPhone Duo is that, in split view, each app puts its controls on its outward side. The left app’s strip is therefore on the left, and it reports a left safe-area inset of 84 points. The right app’s is on the right.
This is a physical side, not a leading/trailing side. In a right-to-left language the strip does not move; the app still puts its controls on the same physical edge. So the code must ask left or right, not leading or trailing, and it must not read the right inset on the assumption that the strip is always there.
Two different questions need two different answers:
- Does this view need to avoid the strip? Ask the view’s own safe-area insets. A page inside a container that has already inset itself will report clean insets, and that is correct.
- Does this screen have a strip at all? Ask the view and the window and take the union. In a split-view pane the view reports {82, 84, 34, 0} while its window reports {0, 0, 34, 0}: the window knows nothing about the horizontal inset. Ask only the window and the pane answers “no strip”; ask only the view and a child that has already been inset answers “no strip” too.
We wrapped both in two helpers: one returns which edge has the strip, the other answers whether the screen has one at all. Every layout decision goes through those two, not through a raw inset.
Let UIKit Own the Strip
The tempting move is to draw your own controls into the strip. Don’t, at least not on pages that have a navigation bar.
From iOS 27.1, UIKit moves navigation-bar items into the strip by itself: the system back button at the top, image-bearing trailing items grouped below it with the 22-point visual gap HIG asks for, and text-only items left in the horizontal bar. In split view it does this on whichever side has the strip. The app does not position any of it.
The app has one job: when a page’s items live in the strip, a text-only right bar button needs an image. “Done” becomes a checkmark; “Edit” becomes a pencil. When the items move back to the horizontal bar, remove the image. The title stays for VoiceOver.
There is a catch that cost us an afternoon. UIKit decides whether an item goes into the strip when the item is handed to the bar. Changing the image afterwards does not make it re-decide; the result is an icon sitting in the horizontal bar. After changing the image, clear the item list and assign it again — set it to nothing, then set the same items back — so UIKit evaluates the item again. Make the helper idempotent, because it runs when the page appears, when the safe area changes and during layout.
The other expensive lesson is the back button. Do not hide the system back button to intercept an unsaved edit: the navigation controller then refuses the interactive pop gesture, and forcing the gesture back on does not restore it. Use the navigation item’s back action (iOS 16 and later) for the interception. The system back button stays, UIKit still moves it into the strip, and the swipe still works.
There are two swipe gestures to think about, not one. Since iOS 26, the navigation controller installs the public edge-swipe gesture and a second content-swipe gesture that starts anywhere in the content, ignores the first gesture’s enabled flag and ignores the back action. An editor with unsaved changes must disable both; every other page can leave both on. Our helper finds the sibling by class rather than by name, because the name is not API.
The less we draw in the strip, the better it looks. On the main screen, which has no navigation bar, we do draw the header controls there — but with the same position formula UIKit uses, so a system button and ours line up.
Edges, Insets, and the Scroll Bar
The rule for full-screen lists is: material bleeds, content does not.
A table, a text view or a web view runs to the physical screen edge, because that is what system lists do and where the scroll indicator belongs. The content inside avoids the strip through the safe area. Pinning the whole table to the safe area instead puts the scroll indicator between the content and the strip — at 382 of 466 points on the outer panel — which looks like a layout bug even though every cell is correct.
Inset-grouped tables need more care. UIKit resets their layout margins during its own layout pass, and in a split-view pane the insets arrive in several steps. Setting the margins once when the safe area changes is not enough: by the time the table draws, they have been reset, and the cells sit against the strip with square corners. Recompute them on every layout pass, use the physical sides (“safe area + 20”), and turn off the flag that derives layout margins from the safe area so the value you read back does not already contain the safe area. Only write when the value changes, or the layout will chase itself.
Text views do not shrink their text by the safe area, so the safe area goes into the text container’s inset. The page container for the dashboard turns off automatic content-inset adjustment because it scrolls horizontally; without that, UIKit folds the strip into the horizontal content inset and shifts every page. A web view is the same story: once you take over its inset adjustment, the page needs a viewport that covers the screen and padding from the safe-area environment variables, or WebKit stops moving out of the strip.
The scroll indicator deserves its own paragraph. UIKit folds the vertical safe area into the adjusted content inset, but not the horizontal one. On the outer panel the indicator is drawn at 460 of 466 — right on top of the vertical strip. The fix is not to add the horizontal inset on top of the automatic adjustment, which applies it twice and pulls the indicator in to 292. Turn off automatic scroll-indicator adjustment and set all four edges yourself: top and bottom from the adjusted content inset, left and right from the safe area. On a screen with no horizontal inset the result is identical to the automatic behaviour, so nothing moves on other devices.
Overlays have the same “screen is not the window” problem. In a split-view pane the app has 469 points while the screen has 951. A toast, HUD or banner positioned against the screen lands outside the app. They are positioned against the active window’s content rect. Action sheets become popovers at Regular width, and a popover without an anchor throws; every one of ours now points at the row that opened it.
The Crease and the Split
The strip is not the only reserved region. In a half-folded state the inner screen reports the crease as a division region — measured at (455.5, 0, 40, 669), twenty points of margin on each side of the curve.
For a single-column screen the crease is cosmetic: content drawn across it is ugly but not hidden. For a split view it is structural. The separator should land in the crease, so the two panes read as two pages of a book. UIKit already tries to do this — unless the app caps the primary column at a width that stops short of the crease, which almost every app does to prevent over-wide rows on iPad.
We measured it: with the primary column capped at 460 and the crease at 475, the separator stopped at 380 and the article was drawn straight across the curve — title, first paragraph and header image folded in the middle. Raising the cap past the crease and letting UIKit choose the width put the separator at 475.7, with the two panes’ safe areas at 455.7 and 495.7, exactly the two edges of the crease.
The other half of the split-view work is state, not geometry. A fold is a state change, not a size change.
- Always hand a detail page to the split view, even when it is collapsed. It looks identical to a push on the phone, but a page pushed onto the list’s own navigation stack is invisible to the split controller. When the device unfolds, the left column shows the article and the right column shows whatever was there before — usually the previous article. Setting the detail column and showing it produces a push when collapsed and a detail pane when expanded, from one call site.
- On collapse, choose the column by what the secondary column actually holds. Always choosing primary throws away the article the user is reading; always choosing secondary can leave an empty detail pane. If the secondary is a placeholder, go to primary; if it holds a real document, stay on secondary.
- Only answer for the selected tab. All tabs’ split controllers collapse at the same time, and the user is standing in one of them. Without checking that the collapsing split controller is the selected one, collapsing in another tab replaces that tab’s list with an article the user is not looking at.
- On expand, do not accept detail-only display unconditionally. If the user opened an article while closed and then unfolded, detail-only fills the wide screen with the article and leaves the list nowhere. Use the split’s preferred display mode instead, so the list comes back beside it.
Testing a Fold
The fold cannot be automated. There is no simulator command for posture, and Xcode’s device tooling does not reliably tap the inner panel. What works is the two live framebuffers — 1398 × 2034 for the outer screen and 2007 × 2853 for the inner — captured with the simulator’s per-display screenshot command, while navigation is driven through the debugger after attaching: push a controller, call an action, continue, capture. It is slow, and it is the only way to see what the layout actually does at the moment the scene resizes.
Two layout-order facts made the debugging easier. The view’s layout callback runs before the subviews are laid out, so measuring a cell’s label there without forcing a layout pass first gives you the previous row’s geometry. And during a transition the scene size is in flux; measuring then gives a number that is true for neither the old nor the new panel.
The Shape of the Fix
The fold is not a bigger phone. It is a phone whose safe area moves, whose width class lies about the device, and whose layout has to re-answer questions it used to answer once at launch.
Once the layout asks the view instead of the screen, and lets UIKit own the strip it created, the rest is consistency: the same safe area in the table, the text view, the overlay and the popover; the same left-or-right answer in every helper; the same split-view state after every fold.
Chute iOS 1.3.0 is available now, with the foldable layout and the rest of the release.
Thanks.
Chute Devs