I got the iPhone Duo simulator running on October 13, while getting my journaling app Lanternly ready for release. The simulator itself turned out to be useful. Getting to it was not. Plain Xcode 27.0 has no Duo device at all. The runtime downloads without a single line of progress. Simulator is gone, DeviceHub took its place, and half of what I needed cannot be done from the command line.
This is everything that helped along the way: how to reach the simulator, how to capture both screens, how to fold and rotate the device from a script, and how to get iPhone Duo screenshots for the App Store. What I had to change in the app itself is covered in the article on iPhone Duo adaptation problems.
Where to get the iPhone Duo simulator#
The Duo device only exists in Xcode 27.1. I installed the Release Candidate (build 27A9275) next to the shipping 27.0 rather than replacing it, and point to it with DEVELOPER_DIR:
export DEVELOPER_DIR=~/Downloads/Xcode.app/Contents/Developer # 27.1 RC
xcrun simctl list devicetypes | grep -i duo
#> iPhone Duo (com.apple.CoreSimulator.SimDeviceType.iPhone-Duo)In Xcode 27.0 the same command finds nothing. "iPhone Duo simulator missing" shows up in search suggestions, and this is the first answer to it: the device type ships with Xcode 27.1, not with the runtime.
The second answer is the iOS 27.1 runtime. Without it the device is listed, but there is nothing to create it on:
xcodebuild -downloadPlatform iOS
#> Downloading iOS 27.1 Simulator (24A94232) (arm64): 22,0% (1,73 GB of 7,85 GB)That is 7.85 GB. My first attempt piped it through | tail -20, and it looked frozen: tail prints nothing until the command ends. Writing the output to a file showed normal progress. To confirm it landed:
xcrun simctl runtime list | grep 27.1
#> iOS 27.1 (24A94232) - … (Ready)Create the device and find both screens#
UDID=$(xcrun simctl create "iPhone Duo" \
com.apple.CoreSimulator.SimDeviceType.iPhone-Duo \
com.apple.CoreSimulator.SimRuntime.iOS-27-1)
xcrun simctl boot "$UDID"
xcrun simctl io "$UDID" enumerateenumerate prints a long list of ports. You want the two screens of type Integrated:
| Screen | Name for --display | Size in pixels |
|---|---|---|
| Outer (folded) | primary | 1398 × 2034 |
| Inner (unfolded) | primary-1 | 2007 × 2853 |
These are exactly the iPhone Duo screenshot slots in App Store Connect. Each screen is captured separately:
xcrun simctl io "$UDID" screenshot --display=primary outer.png
xcrun simctl io "$UDID" screenshot --display=primary-1 inner.pngRight after boot the device is folded, so the inner screen comes out as a black rectangle. That is expected.
DeviceHub instead of Simulator#
In Xcode 27 the Simulator app is replaced by DeviceHub: Xcode.app/Contents/Applications/DeviceHub.app. At the bottom of its window there is a row of buttons: rotate and three postures, folded, book and flat. The Duo posture only changes there.
I was counting on simctl and checked: it has no rotate or fold commands. There is io screenConfig, which can switch a single screen off:
xcrun simctl io "$UDID" screenConfig --display=primary power off
#> Screen 1 power state set to offIt does not change the posture. The outer screen goes dark and the inner one stays black, because the device is still folded.
Clicking DeviceHub buttons from a script#
Next idea: drive DeviceHub with AppleScript. It did not work. System Events sees the process, but reports 0 windows and no usable menu bar, while Finder reads fine in the same session. Plain CoreGraphics mouse events did work. A ten-line Swift tool:
import CoreGraphics
import Foundation
// click x y — click at a screen point (coordinates in points)
let a = CommandLine.arguments
let p = CGPoint(x: Double(a[1])!, y: Double(a[2])!)
let src = CGEventSource(stateID: .hidSystemState)
CGEvent(mouseEventSource: src, mouseType: .mouseMoved, mouseCursorPosition: p, mouseButton: .left)?.post(tap: .cghidEventTap)
usleep(150_000)
CGEvent(mouseEventSource: src, mouseType: .leftMouseDown, mouseCursorPosition: p, mouseButton: .left)?.post(tap: .cghidEventTap)
usleep(80_000)
CGEvent(mouseEventSource: src, mouseType: .leftMouseUp, mouseCursorPosition: p, mouseButton: .left)?.post(tap: .cghidEventTap)Build it with swiftc -O click.swift -o click. I take coordinates from a screencapture -x shot: on a Retina display it is in pixels, so I divide by 2 to get points.
A few small things that cost time:
- Bring the window forward first (
tell application "DeviceHub" to activate) and wait a second or two. Without the pause the first click sometimes went nowhere. - In portrait the posture icons are drawn rotated. The button order is the same, they are just harder to recognise.
- If the body is rotated, the outer screen ends up in landscape after folding. I press rotate until a
--display=primaryshot is 1398 wide. - The DeviceHub menu bar takes the same clicks:
Controls → Home,Device → Touch ID → Enrolled. Keyboard shortcuts sent through System Events never reached it.
Rotation without DeviceHub#
On iPhone and iPad it is easier to rotate from inside the app. I added a debug launch flag that asks the scene for an orientation:
#if DEBUG
// -orientation landscape,portrait — one step every 4 s after launch
static func applyOrientationFlag() {
let args = CommandLine.arguments
guard let i = args.firstIndex(of: "-orientation"), i + 1 < args.count else { return }
for (step, name) in args[i + 1].split(separator: ",").enumerated() {
let mask: UIInterfaceOrientationMask = name == "portrait" ? .portrait : .landscapeRight
DispatchQueue.main.asyncAfter(deadline: .now() + Double(step + 1) * 4) {
for scene in UIApplication.shared.connectedScenes.compactMap({ $0 as? UIWindowScene }) {
scene.requestGeometryUpdate(.iOS(interfaceOrientations: mask))
}
}
}
}
#endifxcrun simctl launch "$UDID" <bundle> -orientation landscape,portrait rotates the screen there and back, and shots between the steps show how the app survives a width change. It worked on the iPhone 18 Pro Max. On iPad under iPadOS 27 the app lives in a window, and the system ignores the rotation request.
iPhone Duo screenshots for the App Store#
The slot sizes match the screen buffers, so raw frames come straight from simctl io screenshot. Before shooting I tidy the device:
xcrun simctl status_bar "$UDID" override --time 9:41 --batteryLevel 100 \
--batteryState discharging --wifiBars 3 --cellularBars 4
xcrun simctl privacy "$UDID" grant microphone <bundle>discharged, which you will find in older cheat sheets, is rejected by simctl. The valid values are charging, charged and discharging.
Three more things only showed up on finished frames:
- System prompts. After reinstalling the app, a microphone permission prompt appeared on the voice recording screen.
simctl privacy … grantremoves it. - Touch ID. Until
Device → Touch ID → Enrolledis ticked, the settings screen honestly says biometrics are unavailable. Rebooting the simulator, for example to change the language, clears that tick. - Debug labels. In my debug build the chat header shows where the reply came from ("· simulator"), and settings show the local storage mode. The
-storeShotslaunch flag hides them for the shoot only. The release build does not change.
And the main rule, which I once got wrong. A device slot only takes frames from that device. Before the Duo simulator existed, I fitted iPad frames into its size, and they had to be deleted. A screenshot promises what a person will see on their screen, and on a Duo they see a different layout. Sizes and formats for the rest of the App Store assets are in the Creative Assets guide.
What did not work#
Split View by gesture. On the inner Duo screen, as on iPad, a second app is dragged in from the Dock. A mouse swipe through CGEvent either opened the app switcher when it was long, or did nothing when it was short. This case is left for a manual check. The logic of a narrow half of the screen is partly covered by the app already running in a narrow window on the outer screen, but that is an inference, not a test.
Postures from the command line. Folded, book and flat are switched only with DeviceHub buttons. If your CI runs on simulators, posture checks cannot move there yet.
If you are testing a Duo from scratch, the order is: Xcode 27.1 next to your main one, the 27.1 runtime with output to a file, simctl create and enumerate, and for postures, DeviceHub plus a small click tool. What broke in the app on these screens and how I fixed it is in the adaptation article. General theory about the device is in the iPhone Duo guide.



