Dev.to · 9 min read

iPhone Duo for iOS Developers: What Actually Changes in Your Swift Code

iPhone Duo for iOS Developers: What Actually Changes in Your Swift Code

Apple announced the iPhone Duo today — its first foldable iPhone. A 5.4-inch outer display, a 7.6-inch inner display when you open it, ships with iOS 27.x. The marketing story is the hardware. The developer story is that a lot of assumptions baked into iPhone apps since 2007 just stopped being true. There is now more than one screen. The screen can change shape while your app is running. The safe area is no longer symmetric. And "portrait only" doesn't mean what you think it means anymore. The good news: if you've been doing adaptive layout properly for iPad, most of this is a short afternoon. If your app hardcodes screen sizes and branches on orientation, you have some work to do. Here's the practical version. First: what happens if you do nothing? Your app still runs. It doesn't crash, it doesn't get pulled. But how much of the screen you get depends on which SDK you last built against: Built with Inner display behavior Older SDK (no rebuild) Runs, but letterboxed — your app keeps its old aspect ratio, with black bars either side iOS 27 SDK Extends left of the status bar, avoiding the camera area iOS 27.1 SDK Full edge-to-edge, and standard nav/toolbar buttons lay out vertically So step zero is genuinely just: rebuild with Xcode 27.1. That alone moves you from "obviously not updated" to "fine." Everything below is the difference between "fine" and "good." The four poses you now have to support This is the mental model shift. It's not portrait vs landscape anymore. It's: Closed, portrait — behaves like a normal iPhone Closed, landscape Open, vertical (tall, near-square) Open, horizontal (wide) Plus the in-between states: partially folded like a book, or propped up in a tent on a table. Your layout has to survive all of them, and it has to survive transitions between them while state is live. Change #1: Size classes, not orientation This is the single most important line in Apple's guidance, and it will break the most apps. The inner display doesn't honor your supported interface orientations. If your app branches layout on UIDevice.orientation or supportedInterfaceOrientations, that logic is now lying to you. Use size classes instead — they describe the space you have, which is the thing you actually care about: // SwiftUI struct ContentView: View { @Environment(\.horizontalSizeClass) private var hSize @Environment(\.verticalSizeClass) private var vSize var body: some View { if hSize == .regular { WideLayout() } else { CompactLayout() } } } // UIKit override func traitCollectionDidChange(_ previous: UITraitCollection?) { super.traitCollectionDidChange(previous) let isWide = traitCollection.horizontalSizeClass == .regular // reconfigure } What to expect on device: Outer display → same size classes as any other iPhone Inner display → regular in both dimensions, which is what leaves room for sidebars and two-column layouts That second row is the one worth internalizing. The inner display is, as far as your layout code is concerned, iPad-shaped. Change #2: Stop referencing the main screen UIScreen.main is ambiguous on a two-display device, and Apple has said it's heading for deprecation. There is no longer one screen. // ❌ Which screen? Nobody knows. let scale = UIScreen.main.scale let bounds = UIScreen.main.bounds // ✅ Ask the trait collection let scale = traitCollection.displayScale // ✅ Or get the screen dynamically from the window scene let screen = view.window?.windowScene?.screen For layout size, prefer the scene's bounds or the environment over anything screen-derived. Do a project-wide search for UIScreen.main right now — it's usually a five-minute fix and it's the most likely source of weird bugs. While you're in there, ConcentricRectangle (SwiftUI) and UICornerConfiguration (UIKit), introduced in iOS 26, will match the actual screen corner radius instead of your hardcoded cornerRadius: 12. Change #3: Safe areas are now asymmetric On a normal iPhone, the left and right insets match, so a lot of code does this: // ❌ Assumes both sides are equal let usableWidth = view.bounds.width - view.safeAreaInsets.left * 2 On iPhone Duo, safe areas and layout margins are frequently asymmetric — the camera is on one side, and in Split View your app occupies half the display with different insets on each edge. Handle each side independently: // ✅ let usableWidth = view.bounds.inset(by: view.safeAreaInsets).width The general rule Apple repeats: interactive foreground content stays inside the safe area; background artwork extends past it. // SwiftUI ZStack { BackgroundArtwork() .ignoresSafeArea() // background: bleed ControlsView() // foreground: respects safe area by default } Change #4: Reserved regions (the hinge and the cameras) This is a genuinely new concept. The device has physical features that carve up the display: the hinge and the cameras. Apple calls these reserved regions, and you can query them. Two kinds: .division — the fold. It splits a larger area into smaller ones. It's only active when the device is actually folded; when flat it has zero width and is inactive. .occlusion — something sitting on top of your content. This is the under-display FaceTime camera. // SwiftUI GeometryReader { proxy in let folds = proxy.reservedRegions(kind: .division) let frames = folds.map(\.frame) MyLayout(avoiding: frames) } // UIKit let folds = view.reservedRegions(kind: .division) let frames = folds.map(\.frame) Inactive regions are excluded by default. Ask for them when you want to make a structural decision that shouldn't flip-flop as the user opens and closes the device — like "always use an even number of grid columns so nothing lands on the fold": let folds = proxy.reservedRegions(kind: .division, options: .includeInactive) When to reach for this: custom, manually laid-out controls. Standard containers — NavigationStack, NavigationSplitView, TabView, List, ScrollView — already adapt to the fold for free, and the system automatically repositions alerts, action sheets, menus and popovers around reserved regions. Don't reimplement that. Design note that's easy to miss: continuously scrolling content (articles, feeds) should not jump around to avoid the fold. Displacement is for discrete elements — a button, a control cluster, a container — not for a reading surface. Change #5: Arrangements — a new layout container New in iOS 27.1. An ArrangementView sits between your navigation container and your content, takes a primary and secondary view, and decides how to place them based on size classes, aspect ratio, and any active division regions. Think Podcasts: now-playing view plus transcript. var body: some View { NavigationStack { ArrangementView { PlayerView() } secondary: { UpNextView() } .arrangementViewStyle(.split) } } Two styles: .split — divides the available bounds between the two views. Horizontally when wider than tall, vertically when taller. Constrain it if you only want one axis: .arrangementViewStyle(.split.axes(.horizontal)) .overlay — prefers stacking content above/below, and moves to side-by-side as the device folds. Read the Z index to adapt the overlaid view: struct UpNextView: View { @Environment(\.overlayArrangementZIndex) private var zIndex var body: some View { UpNextList(minimized: zIndex > 0) } } UIKit equivalent: let arrangement = UIArrangementViewController() arrangement.setViewController(playerVC, for: .primary) arrangement.setViewController(upNextVC, for: .secondary) arrangement.updateArrangement(.split.axes(.horizontal)) let nav = UINavigationController(rootViewController: arrangement) Picking one: if your existing layout is an HStack/VStack, that's split. If it's a ZStack, that's overlay. Otherwise — overlay when there's a clear foreground/background relationship and partially obscuring the background is acceptable; split when it's main/detail and neither side should be covered. Two gotchas: ArrangementView provides no navigation infrastructure, so don't nest a NavigationSplitView inside one. And don't put one inside a List or ScrollView. Change #6: The hinge API You can read the hinge directly. onHingeChange in SwiftUI, UIHingeInteraction in UIKit. Both give you a coarse status (closed / partially open / fully open) and a continuous angle. struct InstrumentView: View { @State private var pitchBend: Double = 0 var body: some View { GuitarView(pitchBend: pitchBend) .onHingeChange { _, context in // nil hinge == device doesn't have one. Always check. guard let hinge = context.hinge, hinge.status == .partiallyOpen else { pitchBend = 0 return } pitchBend = bend(for: hinge.angle) } } } That guard is not optional politeness — your app runs on every other iPhone too, where context.hinge is nil. Important boundary: the hinge API is for interactions and effects — a whammy bar, a parallax, a camera shutter that responds to the fold. It is not for layout. Use arrangements and reserved regions for layout. If you find yourself computing frames from hinge.angle, you're on the wrong API. Change #7: Multiple scenes and Split View Two apps side by side on iPhone, for the first time. Every app participates whether it opts in or not. If you already support resizing on iPad or iPhone mirroring, you're most of the way there — same tools, size classes and scene geometry. For multiple windows of your own app, iPhone Duo is the first iPhone to support it, and apps that already do this on iPad get it automatically. One rule to know: new windows can only be created on the inner display, never the outer one. So handle the failure case: // Hides itself automatically when new windows aren't available UIWindowSceneActivation(...) And actually handle the error path when requesting a scene, rather than assuming it succeeds. Change #8: Scene accessories (the fun one) Scene accessories let your app put content on both displays at once. Main UI inside, supplementary content outside. The headline case is CameraCaptureAccessory, available when your app is full screen on the inner display with an active camera session. The obvious use: showing your subject their own framing on the outer display while you shoot. Or a teleprompter: struct CameraRootView: View { @State private var model = TeleprompterModel() var body: some View { CameraView(model: model) .sceneAccessory { CameraCaptureAccessory(isEnabled: $model.isEnabled) { TeleprompterView(model: model) } .onAvailabilityChange { model.isAvailable = $0 } } .toolbar { TeleprompterToggle(isEnabled: $model.isEnabled) .disabled(!model.isAvailable) } } } The system controls availability dynamically — closing the device takes it away — so wire up onAvailabilityChange and disable your UI accordingly instead of letting users tap into nothing. Testing this without a $1,999 device Xcode 27.1 is out now. iPhone Duo simulator via Device Hub, with on-screen controls to open, close, rotate and fold. This is how you'll actually verify the four poses. App Resizability — the app modernization skill from "Modernize your UIKit app" at WWDC26, renamed in Xcode 27.1, now covering SwiftUI and iPhone Duo. Test Split View explicitly. Half-width plus asymmetric insets is where most layout bugs show up. The checklist Rough order of effort-to-payoff: [ ] Rebuild with the iOS 27.1 SDK [ ] Grep for UIScreen.main and replace it [ ] Replace orientation checks with size class checks [ ] Fix any safeAreaInsets.left * 2 style symmetry assumptions [ ] Move custom navigation to NavigationSplitView / TabView; consider .defaultTabBarPlacement(.sidebar) on the inner display [ ] Run every screen through all four poses in the simulator [ ] Audit centered layouts — would a two-column layout be better? [ ] Adopt ArrangementView for custom split/overlay layouts [ ] Adopt reserved regions for your highest-priority hand-laid-out controls [ ] Consider the hinge API and scene accessories if your app has a reason to use them The first four are cheap and prevent embarrassment. The rest are where the actual opportunity is — the Duo is going to make it extremely obvious to users which apps got attention and which didn't. Further reading Apple's Tech Talks for the Duo: Prepare your app for iPhone Duo

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More Programming & Dev News