How to Add iPhone Duo Support to React Native with Expo
Beto, October 2026
I updated Inkigo, my React Native and Expo app for creating tattoos and previewing them on your body, to run on iPhone Duo. A lot already worked. Native tabs, toolbars, and form sheets adapted as I folded and rotated the phone.
The part that needed more attention was our custom layout. To understand those changes, start with safe areas: the space your content gets after the system makes room for its controls.

Premium resourceBuild with Inkigo's source code
The complete app is included with Pro
Give your agent access to this project with the Code with Beto MCP. Connect it to Claude Code or Codex, then use this prompt:
Use the Code with Beto MCP to copy Inkigo's onboarding into my Expo app, then help me add iPhone Duo support.
1. Enable landscape, rebuild, and look at what already works
Check first. If your app is locked to portrait, allow both orientations:
{
"expo": {
"orientation": "default"
}
}Inkigo already had this value. Next, rebuild the native app with Xcode 27.1 and the iOS 27.1 SDK. Reloading JavaScript does not update the SDK inside your development build.
Keeping both orientations enabled is our choice for Inkigo. An orientation lock does not replace adaptive layout: in resizable contexts such as iPhone Mirroring, iOS 27 can treat it as a preference. Measure the space your view receives and test both displays. Expo explains the orientation behavior here.
This is my early demo after the update, before the custom layout refinements below:
The starting point: Inkigo's existing native navigation adapting as the phone opens, closes, and rotates.
Watch the tabs move from the side to the bottom in the tall inner-display pose. The toolbar moves too, and the form sheet becomes a floating sheet when there is more room.
We get that behavior because Inkigo uses Expo Router's native tabs, native stack toolbars, and . I did not write a separate navigation layout for each pose. Your starting point will depend on how much of your app uses native components and how much you draw yourself.
That was great to see. It also made the remaining custom-layout issues easier to spot.
2. Understand why the safe area moves to the side
A safe area is an inset rectangle inside your view. It helps keep content clear of things such as the camera, status area, and navigation controls. In React Native, you can read its four edges with :
const { top, right, bottom, left } = useSafeAreaInsets();On iPhone Duo, controls that usually sit at the top and bottom often move to a vertical strip. That leaves more height for your content, but it also means the left and right insets may be different. The inner display in portrait is an exception: it has room for horizontal bars.

Do not turn that into “always add padding on the right.” Controls can appear on the other side, including in Split View. Read the current insets and handle each edge independently.
Also, a safe inset is not a command to paint an empty black strip. You can let artwork extend behind system controls while keeping buttons and readable content clear of them. That distinction explains the next two screens.
3. Let artwork fill the view, and let forms respect the controls
On Home, the tattoo rails can continue behind the vertical controls. I want the images to fill the screen. Settings has a different job: its rows and switches need a clear, predictable area where you can read and tap them.
Home: artwork behind the controls
Settings: rows inside the safe area
Home keeps its outer scroll view and artwork full width. Each horizontal image rail adds its own end padding, so the first and last cards can move completely clear of either side's controls:
const { left, right } = useSafeAreaInsets();
<LegendList
horizontal
data={data}
renderItem={renderItem}
contentContainerStyle={{
paddingLeft: left + 16,
paddingRight: right + 16,
}}
/>;This matters in our current React Native version: does not handle the horizontal safe area by itself yet. Expo's Duo guide calls out the pending React Native fix. Put horizontal clearance on the scroll content, not around the whole viewport. Artwork can still pass under the rail while scrolling.
Our iOS Settings screen uses Expo UI's native SwiftUI inside a . We keep its normal safe-area behavior:
<Host style={{ flex: 1 }}>
<Form>{/* Native sections, rows, and toggles */}</Form>
</Host>Expo UI also exposes for SwiftUI content. You can choose an edge, such as .
The important part is which view owns the spacing. That modifier affects a SwiftUI view. It cannot remove padding we already put on a React Native parent. Our onboarding screen is an example of that.
4. Center onboarding without taking away the Back button's space
The onboarding form looked fine when closed. On the unfolded display, the question, options, and Continue button leaned left, leaving more empty space on the right. In this capture, the column sat 42 points left of center.
Before
After
We were adding the safe-area padding to the parent, then centering a column inside it:
paddingLeft: left,
paddingRight: right,That centers the form inside the remaining safe rectangle. With a larger right inset, the rectangle's center is left of the screen's center. The layout was following our instructions; those instructions were not the result we wanted on the wide display.
I kept the width limit, started from the display's center, and constrained that position to the safe edges:
const contentWidth = Math.min(560, width - left - right);
const centeredLeft = (width - contentWidth) / 2;
const paddingLeft = Math.min(
Math.max(centeredLeft, left),
width - right - contentWidth,
);
const paddingRight = width - paddingLeft - contentWidth;Here, comes from the foreground container's . The title, options, and Continue action share the result. The full helper keeps the original insets until the view has a usable measurement.
When the phone is closed, we keep the existing safe column. These steps have a native Back button, and our custom content should not compete with that control's space. Removing the right padding everywhere would lose that protection.
You can ask Claude Code to use Inkigo as a reference and apply this to your own forms:
Use the Code with Beto MCP to read Inkigo's onboarding. Center my app's forms on the unfolded iPhone Duo, keeping them clear of native controls when folded.
5. Give content more room when it can use it
The opening screen has no Back button. Its title, Continue button, and Sign In link sit below the system controls, but they were still inheriting the same side inset as the question steps.
We can give that lower content more horizontal room when it fits beneath the controls:
Before
After
Save Your Progress follows the same idea. It has no Back button either, just progress near the top. The title and Apple and Google sign-in buttons form a block near the bottom.
Before
After
Both screens follow the same rule: the entire bottom block must fit in the lower half of the foreground. That leaves the upper half for artwork, progress, and system controls.
For Save Your Progress, we measure the title and sign-in buttons together. For the opening screen, we measure its title and description, then include the whole footer: Continue, Sign In, the gaps, and bottom clearance. The existing view measurements give us that footer height:
const introFooterHeight = introContentFrame
? foregroundSize.height - introContentFrame.y - introContentFrame.height
: 0;We keep the title measurement from the normal inset width and read the footer from its current frame. A new width, font scale, or bottom inset requires fresh measurements. This lets the footer follow resizing without making the title’s line wrapping decide its own width.
Once the measurements are current and the content fits, either of these two screens can opt in:
const useFullWidthBottomBlock = useFullWidthAuth || useFullWidthIntro;
const horizontalPadding = useFullWidthBottomBlock
? { paddingLeft: 0, paddingRight: 0 }
: getOnboardingHorizontalPadding(foregroundSize.width, left, right);The column still stops at 560 points. Short layouts retain the usual side insets. On the sign-in step, the wider block stays bottom-aligned and cannot scroll up into the top controls; its inset fallback can scroll when needed.
The opening screen revealed one more detail: Continue is a SwiftUI button inside an Expo UI . Centering its React Native parent was not enough. The Host still applied its own safe-area clearance, leaving the button to the left after the title had centered.
We let that Host fill the bounds its parent already calculated:
<Host
ignoreSafeArea={parentHandlesSafeArea ? "container" : undefined}
style={{ width: "100%", height: 60 }}
>
{/* Native Continue button */}
</Host>is our own opt-in prop, passed by the onboarding footer. It keeps one place responsible for the spacing. The native Settings form keeps its normal safe-area behavior.
The lower-half rule is an Inkigo layout choice, not a system guarantee. Safe-area insets do not describe an expanded Dynamic Island or the active fold. For custom controls that need that precision, exposes those regions. We have not integrated it here, so expanded regions and partially folded layouts still need a dedicated pass before treating this wider layout as fully verified.
For a sign-in screen like this, give your agent the screen name and the behavior you want:
Use the Code with Beto MCP to read Inkigo's Save Your Progress screen. Adapt it to my app: use the full width below iPhone Duo's controls when it fits, with a scrollable fallback.
6. Test the custom parts across poses
Native navigation gave us a good starting point. Custom views still need to react to the space they actually receive.
Measure the current view
A module-level captures one moment. Use for window-level decisions, and for a photo, slider, or list inside another view:
const [size, setSize] = useState({ width: 0, height: 0 });
<View
style={{ flex: 1 }}
onLayout={({ nativeEvent: { layout } }) => {
setSize({ width: layout.width, height: layout.height });
}}
>
<Image
source={photo}
style={{
width: Math.min(420, size.width),
height: Math.min(size.height, 460),
}}
/>
</View>;In Inkigo, preview stages measure their available space, and lists use their own width. The before-and-after slider stores a fraction from to , so a reveal at 70% can remain at 70% after resizing. Its handle stays in the artwork above the title.
Keep short forms usable
Closed landscape with a keyboard leaves very little height. The name field originally disappeared while Continue stayed visible:
Before
After
We put the step body inside a and kept Continue outside it. The surrounding comes from . Advancing dismisses the keyboard and resets the scroll position for the next step.
The input's outer glow was also getting clipped. Moving the animation onto the input and using an inset shadow kept the effect inside its bounds, without adding more padding:
boxShadow: "inset 0 0 16px 0 rgba(255, 255, 255, 0.35)";Before shipping, fold and rotate with a prompt or photo already on screen, then check a partial fold, both Split View positions, and a standard iPhone. Keep the same content and reachable controls throughout. Our checks so far cover folded and open layouts; the remaining cases need their own pass. I cover the React Native and Expo fundamentals behind these decisions in my course.

CourseBuild and ship with React Native
Learn React Native and Expo through practical, step-by-step lessons.