Black Friday30% off your first month. See prices →
VibeView

Agentic development · iPhone Duo

An AI agent adds iPhone Duo support to an Expo app

Rokas Kašinskas — Co-founder, VibeViewPublished 2026-09-24Updated 2026-09-24

I run VibeView, so read this with that in mind. A week earlier an agent built Kitlist and shipped it to TestFlight in 43 minutes. Then Apple announced the iPhone Duo, its first foldable, and the obvious question for anyone with a React Native app was: what does my app do on it, and how would I even check? Folding a simulator is a mouse gesture in Xcode’s Device Hub; nothing in Apple’s command-line tools does it. So I gave the same app to a fresh agent with one job: make Kitlist good on the Duo, and prove it on the device. This run deliberately stays small: no native code, a layout driven by window size alone, and stable Expo SDK 57. A companion run on our showcase app, Atlas, went further and reads the fold’s exact position through a native module.

The setup

The rules were the ones from the earlier runs. The vibeview CLI, its agent skill and the public docs are the only source of truth about VibeView. Keep a timestamped run log. Fix your own project’s problems and write them down. The agent updated the CLI to 0.23.0 and refreshed its skill with vibeview agent-setup, which now carries a section on foldables. That gave it two verbs and two flags it had not had in the first run:

vibeview list-devices --models                 # the Duo is the row marked "foldable"
vibeview dev --detach --json --platform ios --model "iPhone Duo"
vibeview set-posture closed|partial|open        # or: set-posture --angle 0-180
vibeview rotate                                 # a quarter turn clockwise, in any posture

A Duo session starts closed, on the cover screen. Every session in the run was ready in 23 to 24 seconds.

The iPhone Duo in VibeView is Apple’s own iPhone Duo simulator on the iOS 27.1 beta runtime, streamed to the browser. It is a simulator, not hardware: layout, posture and orientation behave as on the device; hinge feel, sensors and performance are not simulated. See the iPhone Duo simulator page.

What Apple asks for, and what React Native has

The agent read before it wrote. Apple’s guidance for the Duo boils down to three things: size your layout to the window, not the device; a split view should collapse to one pane on the small outer screen and expand on the large inner one; and keep content off the fold, which runs down the middle of the inner screen. Apple gives native apps an API for the fold’s exact position.

React Native has no fold API, no posture API and nothing for that reserved region. The agent checked the Expo SDK 58 preview too: its iOS 27 work is the scene life cycle and scene-aware geometry, and its changelog promises deeper Duo integration “as soon as the iPhone Duo SDK is available to developers”. Expo’s display-features module was an unpublished draft. So it stayed on stable Expo SDK 57 and wrote down why: SDK 58 would add a preview Expo on a React Native release candidate and give Kitlist nothing Duo-specific today.

What React Native does have is enough, if the layout follows the window:

  • useWindowDimensions() re-renders on every fold, unfold and rotation, so one small useLayout() hook decides everything. Two panes when the window is at least 600 points on its short side and 640 on its long side; side by side when the window is wider than tall, stacked when it is taller than wide. The cover screen is about 466×678 points, so it always gets one pane. The inner screen is about 951×669, so it gets two.
  • The window is split 50/50 around a 40-point gutter. The Duo’s fold runs through the centre of the inner screen in both orientations, so a 50/50 split puts only the gutter on the hinge. It is the React Native stand-in for Apple’s split arrangement until there is a fold API to read. The Atlas run, which does read the fold, found it 40 points wide at x 456: exactly where Kitlist’s gutter sits (1368 to 1488 pixels).
  • Safe-area insets are read per edge and applied only to the edges a pane actually touches.
  • The selected trip lives in the route, /trip/[id], not in the view tree. With one pane that route is the packing list with a ”‹ Trips” button; with two it is the list on one side and the packing list on the other. Folding mid-task keeps the same trip on screen because nothing about the selection depends on which layout is showing.
  • Sheets open inside their own pane when there are two, never across the fold.
  • The portrait lock came off. The inner screen ignores it anyway, and apps are resizable on iOS 27.

One piece of configuration was needed beyond layout: iOS 27 requires UIKit’s scene life cycle. Expo SDK 58 does that by default; for SDK 57, Expo backported it as a first-party switch, ios.enableSceneSupport in expo-build-properties. One plugin entry in the app config.

The cover screen found two bugs

The first surprises were on the cover screen.

The cover’s safe area reserves an 84-point strip down the whole right edge for a status bar that sits only at the top. Kitlist’s content respected that inset, but its round + button did not: the tree put it at x 1168–1336 of a 1398-pixel-wide screen, inside the reserved strip, and the screenshot agreed. An absolutely positioned view ignores its parent’s padding, and the button only ever added the bottom inset. The fix was one line, right: 20 + insets.right: a simple, conservative choice that keeps the button clear of the whole strip (the Atlas run reads the exact status-bar region instead).

The second bug was in the Add item sheet. The cover screen is under 680 points tall, and with the keyboard up that is not much. The sheet’s Save button (“Add to …” and the trip’s name) lived inside the sheet’s scroll view, so the keyboard pushed it out of sight: the tree put it at y 1200–1344 while the sheet itself ended at 1264, and a tap on it saved nothing. The agent moved Save into a footer outside the scroll view that always sits above the keyboard, and re-ran the flow: Save now at y 1008–1152, inside the sheet, and “Sunscreen lotion” added under Toiletries. The same bug would have hit anyone on an iPhone SE-sized phone. The Duo’s cover just made it obvious.

The Add item sheet on the Duo cover screen with Sunscreen lotion typed in and the keyboard up; the category chips are visible but there is no save button anywhere on screen.
Before: the save button is under the keyboard.
The same Add item sheet after the fix, with Toiletries selected and an orange Add to Lisbon, long weekend button above the keyboard.
After: the button sits in a footer above the keyboard.

Unfolding, and the layout the tree described

With the fixes in, the agent ran the flow it cared about: pick the Lisbon trip on the cover, add an item, then unfold.

vibeview find "trip.row.lisbon" --session <id>    # → @e3
vibeview tap @e3 --session <id>
vibeview set-posture open --session <id>
vibeview ui-tree --session <id>

The tree after set-posture open covered the whole inner screen, and it described exactly the layout the code intended: the trips list from x 0 to 1368, the packing list from 1488 to 2856, and the 120-pixel gutter between them centred on 1428, half the window, which is where the hinge is. The Lisbon row was marked [selected], its count read 6 / 12, the right pane read 6 left to pack with “Sunscreen lotion” in it, and there was no back button, because with two panes there is nothing to go back to.

Kitlist on the unfolded iPhone Duo in landscape: the Trips list on the left with Lisbon, long weekend highlighted, and Lisbon's packing list on the right, with a thin divider down the middle where the fold is. Each pane has an orange plus button beside its title.
Open, 180°. The trip picked on the cover is still selected; the fold runs through the gutter. Partly folded at 110° or 75° the layout is identical. (Retaken later on Kitlist 1.0.2, with the + in each pane's header; see below.)

A launch-day gap on the unfolded screen

That tree was the only view the agent had of the unfolded Duo during the run. The Duo was a brand-new kind of device for VibeView, and one piece was not finished: its inner screen. The agent’s taps did not reach it and its screenshots came back black. tap @e3 on the Lisbon row with the Duo open answered that the screen looked visually similar, and nothing had changed when the agent checked the tree. It went into the log as a VibeView bug, with the commands, the responses and the sizes.

It did not stop there. The tree on the inner screen was complete and exact, so it made every assertion from the tree, where a pane’s rectangle, a row’s [selected] state and a label’s text are exact numbers and strings, and it did its tapping and typing on the cover. That happens to be the question that matters most for a foldable anyway: does what you did folded survive unfolding?

The gap was fixed the same day. Screenshots of an open or partly folded Duo now capture the screen that is lit, and an agent’s taps go to the unfolded screen when the Duo is open. It was confirmed on a fresh Duo session running the same build; the inner-screen screenshots in this article were taken after that fix.

Part-open, rotated, and closed again

Then it walked the Duo through every posture and orientation it could reach, reading the tree after each step. Each set-posture response names where the device ended up, posture, hinge angle and lit screen, so the agent always knew which state it was asserting against:

vibeview set-posture --angle 110 --session <id>   # partial, 110°, inner screen
vibeview set-posture --angle 75 --session <id>    # partial, 75°, inner screen
vibeview set-posture open --session <id>
vibeview rotate --session <id>                    # a quarter turn, still open

One thing it learned on the way is worth knowing if you script this yourself. The device decides which screen to light, with hysteresis, the way the hardware does. Opening to 75° from closed leaves the Duo on the cover, and the response says closed. Coming down to 75° from 110° keeps the inner screen lit, and the response says partial. The agent reached 75° from 110° for that reason, and read the response every time rather than assuming. The hinge-angle guide covers this in detail.

The second pass, the one the table records, took under six minutes of device time:

#Posture and orientationHowLayout from the treeSelection and state
1Closed, portraitsession start1 pane; + inside the safe area—
2Closedopen Lisbon, add “Sunscreen lotion”1 pane; Save inside the sheet6 left to pack, item under Toiletries
3Open, 180°set-posture openside by side, gutter centred at half the windowLisbon [selected], 6 / 12, no back button
4Partial, 110°set-posture --angle 110side by sidesame
5Partial, 75°set-posture --angle 75side by sidesame
6Open, rotated 90°set-posture open, rotatestacked, window 2008×2856, gutter y 1368–1488same
7Partial 110°, rotatedset-posture --angle 110stackedsame
8Partial 75°, rotatedset-posture --angle 75stackedsame
9Partial 75°, rotated backrotate --degrees 270side by side againsame
10Closed, from partialset-posture closed1 pane, ‹ Trips presentLisbon, 6 left to pack
11Back to the list, then opentap ‹ Trips, set-posture openside by side; right pane “No trip selected”nothing selected
12Closed, rotated 90°set-posture closed, rotate1 pane, landscape—

Twelve of twelve. An earlier pass in the same run covered ten layout and state checks, including ticking Swim shorts on the cover and finding it still ticked after a partial fold and a close.

Kitlist on the open iPhone Duo rotated a quarter turn: the trip list on the top half, Lisbon's packing list on the bottom half, with the fold running across the middle; each half has an orange plus button beside its title and its list runs to the bottom of the pane.
Rotated, identical open and partly folded at 110°: the panes stack, and the fold runs across the gutter. Kitlist 1.0.2.

The screenshots caught one more bug after the run, one the tree-based checks had passed. In the stacked layout the top pane is only about 450 points tall, and its floating + button sat on top of the last trip row and hid its count. The list had bottom padding for the button, but padding only helps once you scroll to the end; at rest the row was still under it. In a pane that short, a floating button always covers a row, so Kitlist 1.0.2 moves the + into each pane’s header, beside the title, whenever there are two panes, and the lists run the full height of the pane. With one pane (the cover screen, a phone) the floating + stays in the corner. The open, rotated, no-trip and iPad screenshots in this article are from 1.0.2.

State 10 is the easy one to get wrong. Folding a two-pane app back to one pane has to decide what that one pane shows. Because the selection lives in the route, the answer falls out: a trip is selected, so the cover shows that trip’s packing list, with ”‹ Trips” to get back. And when nothing is selected, state 11, unfolding shows the list next to “No trip selected. Pick a trip to see its packing list.”

The Duo cover screen showing Lisbon, long weekend with a Trips back button, 5 left to pack, and the packing list grouped under Documents, Clothes and Toiletries.
Folded with Lisbon selected: one pane, the same trip, a way back to the list. From the earlier pass, before the + button moved out of the reserved strip.
The Duo cover screen after closing from a partial fold, still on the Lisbon trip with 4 left to pack and Swim shorts ticked.
Closed again after a partial fold: Swim shorts, ticked on the cover before folding, is still ticked. Retaken on 1.0.2.
Kitlist on the unfolded iPhone Duo with the trip list on the left, an orange plus button beside the Trips title, and a No trip selected message on the right.
Unfolded with nothing selected: the right pane says so instead of guessing.

The same code on an iPhone and an iPad

A layout driven by window size is not Duo-specific code, so the agent checked that it had not broken the phone and turned on iPad support while it was there, since Apple’s own advice is that an iPad-ready resizing layout is most of the Duo work. On an iPhone 17 Pro the app stayed one pane in portrait and in landscape, where the window is 875×403 points and too short for two. On an iPad Pro 11-inch it showed stacked panes in portrait and side by side in landscape, and the Add item sheet opened inside the right pane only.

Kitlist on an iPad Pro 11-inch simulator in landscape: the trip list on the left with Lisbon highlighted by an orange border, and Lisbon's packing list on the right, separated by a thin divider, each pane with a plus button beside its title.
The same two-pane layout on an iPad in landscape (Kitlist 1.0.2).

What was not done

Store submission was skipped this time. The path from a production build to TestFlight and Google Play is the same as in the first Kitlist run, which covers it command by command.

The 50/50 split is an approximation of the fold, not a reading of it. It holds because the fold is at the centre of the inner screen in both orientations, and the tree confirmed the gutter sits there. When Expo or React Native ship a fold API, the split can be refined to the fold’s exact frame without changing anything else.

And this is a simulator on a beta runtime. It answers layout, posture and orientation questions well. Hinge feel, sensors and performance need hardware.

What a person did

During the run, two messages: a change of plan that dropped store submission for this run, and a request to decide, with evidence, whether Kitlist should move to the Expo SDK 58 preview. After it, we fixed the inner-screen gap, rolled it out and confirmed it on a fresh session. A review of the screenshots then found the + button bug in the stacked layout, fixed in Kitlist 1.0.2 and retaken above.

The numbers

Wall clockabout an hour
Session start on the Duo23 to 24 seconds
Posture change3 to 4 seconds; the first unfold of a session took longer
Duo states asserted22 across two passes, all passed
App bugs found on the Duo2 in the run, both on the cover screen, both fixed; 1 more from the screenshots afterwards, fixed in 1.0.2
New dependencies1, expo-build-properties, for the scene life cycle switch
Native code writtennone
VibeView gap that blocked the runinner-screen taps and screenshots, launch day, fixed the same day
Commits9, pushed to the public repository

Frequently asked questions

Does a React Native app need a fold API to support the iPhone Duo?

Not to get the basics right. A layout that follows useWindowDimensions() re-renders on every fold, unfold and rotation, and a 50/50 split keeps content off the fold because the fold sits at the centre of the inner screen. A fold API would let you place the split exactly; Expo’s is not published yet.

Should an Expo app move to SDK 58 for the Duo?

Not for the Duo alone, as of this run. The Duo-relevant part of SDK 58 today is the scene life cycle, and Expo backported that to SDK 57 as ios.enableSceneSupport in expo-build-properties. SDK 58 was a preview on a React Native release candidate. The Atlas run took the upgrade instead and wrote down what it cost.

Can I check the fold states myself without an agent?

Yes. The same set-posture and rotate commands work from a terminal or a CI job, and in the browser the Device tab folds the Duo to a preset or an exact angle. The hinge-angle guide has the details.

Which agent was this?

Claude Code, running the vibeview CLI in a shell, with the public agent skill installed by vibeview agent-setup. The same verbs are available as MCP tools for agents without a shell.

Try it on your app

Install the CLI, run vibeview agent-setup, start a session with --model "iPhone Duo", and ask your agent to fold it. Kitlist is public, and its README has an iPhone Duo section with the layout rules above and the commands to try each posture by hand.

Published 24 September 2026 · Every command and figure above is taken from the run log of 24 September 2026

Try it yourself

Open a live iOS simulator, Android emulator, Apple TV, Android TV or Roku in your browser.

Start free