iPhone Duo for developers

Everything that breaks on iPhone Duo (and the fixes)

Your app opens as a phone-width box in the middle of the screen. Maybe it ignores taps. Maybe the content runs under the clock. These are the seven problems we hit shipping a real app on the Duo, and exactly how to fix each one.

UIKit, SwiftUI and React Native. Every fix here was tested on the iPhone Duo simulator (Xcode 27.1).

A notes app boxed to phone width in the middle of an unfolded iPhone Duo
Before: boxed to phone width
The same notes app on iPhone Duo with the list on the left and the note on the right
After: list and note side by side

Check your app in 10 seconds

Run this from your project folder. It reads your code and lists what will break on the Duo. It doesn't change anything.

curl -fsSL https://panewise.dev/check.sh | bash

Free. Works on Xcode projects, SwiftUI, React Native/Expo and Flutter. Read the script first if you like.

1.The app ignores every tap

Boxed app, dead taps, Apple's sheets still work

Without a UIApplicationSceneManifest in your Info.plist, UIKit runs the app in the legacy window mode and the Duo boxes it. On our app, every tap in our own UI died, while Apple's system sheets on top kept working, which makes it very confusing to debug.

UIKit: adopt scenes. Add the manifest and a SceneDelegate:

<key>UIApplicationSceneManifest</key>
<dict>
  <key>UIApplicationSupportsMultipleScenes</key>
  <false/>
  <key>UISceneConfigurations</key>
  <dict>
    <key>UIWindowSceneSessionRoleApplication</key>
    <array>
      <dict>
        <key>UISceneConfigurationName</key>
        <string>Default Configuration</string>
        <key>UISceneDelegateClassName</key>
        <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
      </dict>
    </array>
  </dict>
</dict>

If a framework needs a key window at launch, adopt the AppDelegate's existing window in the scene delegate instead of building a second one. And forward openURLContexts from the scene delegate, or your deep links quietly stop working.

SwiftUI: the App life cycle is scene-based, but a hand-written Info.plist still needs the manifest, with no delegate class. Naming a SceneDelegate that doesn't exist breaks launch:

<key>UIApplicationSceneManifest</key>
<dict>
  <key>UIApplicationSupportsMultipleScenes</key>
  <false/>
  <key>UISceneConfigurations</key>
  <dict/>
</dict>

XcodeGen or Tuist: Info.plist is rewritten on every generate, so put the keys in project.yml / Project.swift, not the plist.

2.Still boxed after fixing it

Manifest is there, app is still letterboxed

The binary has to be built with Xcode 27.1 or later. Anything older runs on the Duo in compatibility mode. You can keep your current Xcode beside it and build with DEVELOPER_DIR=/Applications/Xcode-27.1.app/Contents/Developer. Check your cloud build image too: if EAS or CI builds with an older Xcode, your store build ships boxed.

3.Content runs under the clock

Text and buttons cut off on the right

On the Duo the status bar is a strip down the right edge, so the safe area has a right inset (about 84pt unfolded). No iPhone has had one before, so most apps ignore it. Pad your screens with the left and right insets too, not only top and bottom. Anything sized from the window width (pagers, carousels, UIScreen.main.bounds, Dimensions.get('window')) comes out one strip too wide. Size it from its container instead. Check full-screen modals separately: they often start a new safe-area context.

4.The left pane is short on the right

Separators stop 84pt early in the left column

The sneaky one. In a hand-built two-pane container, a view controller inside a UINavigationController in the left pane inherited the strip's right inset, even though that pane doesn't touch the right edge. Every view above it reported 0. Wrapping it didn't help, and neither did a negative inset on the navigation controller. What worked: measure the leaked inset on the child and cancel it there, in the container's viewDidLayoutSubviews:

// How much right inset the left pane's child inherited from the status strip.
let inherited = child.view.safeAreaInsets.right - child.additionalSafeAreaInsets.right
child.additionalSafeAreaInsets.right = unfolded ? -inherited : 0

5.The tab bar moved to the right edge

On iOS 27.1 a native UITabBarController or SwiftUI TabView turns into a vertical rail on the right edge, next to the status strip, by itself. That's fine for simple apps. If you add two-pane screens, a left sidebar usually reads better, so the list sits next to its navigation. JavaScript tab bars (React Navigation's JS tabs, custom bars) don't become a rail. They stay a bottom bar stretched across the whole screen, so build the sidebar yourself.

6.SwiftUI: the width check never passes

The usual "unfolded" rule is an iPhone whose window is at least 600pt wide. But a root GeometryReader reports the safe-area width, about 867pt unfolded instead of 951. Add the insets back before you compare:

GeometryReader { proxy in
    let width = proxy.size.width + proxy.safeAreaInsets.leading + proxy.safeAreaInsets.trailing
    let unfolded = UIDevice.current.userInterfaceIdiom == .phone && width >= 600
    // choose the layout from `unfolded`
}

And note that iPad-only checks (userInterfaceIdiom == .pad, Platform.isPad) never fire on the Duo. It's a phone. Add an unfolded branch beside them instead of reusing the iPad one.

7.Folding loses your place

The window resizes while the app runs, so the layout has to switch live. Keep the selected item in state above the layout switch: open note 3, fold, and note 3 should still be open full-screen. Swap the arrangement instead of remounting the screen, so scroll position and half-typed text survive. And when you unfold with nothing open, show the first item without recording it as selected, or folding again jumps into a detail the user never opened.

Testing it: there's no simctl command to fold the simulator. Use Device Hub's toolbar. And simctl openurl raises an "Open in…?" prompt that survives a reinstall, so headless link tests stall unless you add a debug launch argument instead.

Or let Claude Code do all seven.

Panewise is a Claude Code skill. Say "make my app ready for iPhone Duo" and it runs the check, fixes the blockers and the status strip, and builds two-pane layouts that survive folding. A real run took 2 minutes 55 seconds.

Get Panewise · $29

One-time payment. Use it on every app your team builds.