BS
BleepingSwift
Published on
8 min read
Intermediate

> Keeping Content Clear of the iPhone Duo Fold with ReservedRegion

Share:

Safe areas cover the edges of the screen, but iPhone Duo has hardware that gets in the way in places a safe area can't describe. There's a camera in the corner of the outer display, another camera behind the inner display that only shows up when it's in use, and a fold across the middle of the inner display whenever the device is partly open. Nobody wants to tap a button that's sitting on a crease.

iOS 27.1 describes all of these as reserved regions, and gives you an API to ask where they are. Before you reach for it, though, it's worth knowing how much the system already does for you.

The Regions You'll Run Into

Every reserved region has a kind. An occlusion is an area where something covers your content, such as a camera or the Dynamic Island. A division is an area where content should split into two separate parts, which on iPhone Duo means the fold.

Apple's Designing for iPhone Duo guidance lists three regions on the device. The outer front camera is always there, and it grows into the Dynamic Island when a Live Activity is running. The inner front camera only appears when it's active, and the interface moves aside to show where it is. The fold is conditional. When the device is partially open, it splits the inner display into two usable areas with a strip in the middle that content should avoid.

A region can also be active or inactive. The fold is a good example. It's active when the device is partially open and inactive when it's fully open, even though the crease is still physically there.

Check Whether You Need It at All

Most apps won't need to touch this API. Alerts, context menus, and sheets move away from the fold on their own. NavigationSplitView adjusts its column widths and margins so each pane stays clear of it. Bars on the side account for the outer camera automatically, and ArrangementView handles two-pane layouts around the fold.

ReservedRegion is for content you position yourself: a floating tool palette, a custom grid, a game's HUD, or anything laid out with absolute positions or a custom Layout. If none of those exist in your app, you can stop reading here and spend your time in the simulator instead.

Querying Regions in SwiftUI

In SwiftUI, reserved regions come from a GeometryProxy. The reservedRegions(kind:options:layoutDirectionBehavior:) method returns the regions of the kind you ask for that intersect the view. Each ReservedRegion has a frame in that view's coordinate space, a margins value, an isActive flag, and an id.

The frame already includes the margins, which exist so interactive content keeps a comfortable distance from the region. That's what you want for buttons. If you're placing something decorative, like part of an image, you can inset the frame by margins to work with the tighter area.

One detail worth knowing is layout direction. The camera doesn't move when someone switches their device to Arabic or Hebrew, but SwiftUI flips layout geometry for right-to-left languages. By default the method mirrors region frames for right-to-left layouts so they line up with the rest of your SwiftUI geometry. If you ever need the physical position instead, pass .fixed as the layout direction behavior.

Moving a Palette Off the Fold

Here's a drawing screen with a tool palette floating at the bottom center. When the device is partially folded like a book, the fold runs straight through that bottom center spot, so the palette needs to move to one side:

Swift
struct SketchScreen: View {
    private let paletteSize = CGSize(width: 280, height: 56)

    var body: some View {
        GeometryReader { proxy in
            let folds = proxy.reservedRegions(kind: .division).filter(\.isActive)

            ZStack {
                SketchCanvas()

                ToolPalette()
                    .frame(width: paletteSize.width, height: paletteSize.height)
                    .position(paletteCenter(in: proxy.size, avoiding: folds))
            }
            .animation(.snappy, value: folds.map(\.id))
        }
    }

    private func paletteCenter(in size: CGSize, avoiding folds: [ReservedRegion]) -> CGPoint {
        var center = CGPoint(x: size.width / 2, y: size.height - paletteSize.height / 2 - 24)

        for fold in folds {
            let palette = CGRect(
                x: center.x - paletteSize.width / 2,
                y: center.y - paletteSize.height / 2,
                width: paletteSize.width,
                height: paletteSize.height
            )
            guard fold.frame.intersects(palette) else { continue }

            if fold.frame.height > fold.frame.width {
                let leadingWidth = fold.frame.minX
                let trailingWidth = size.width - fold.frame.maxX
                center.x = trailingWidth >= leadingWidth
                    ? fold.frame.maxX + trailingWidth / 2
                    : leadingWidth / 2
            } else {
                center.y = fold.frame.minY - paletteSize.height / 2 - 16
            }
        }

        return center
    }
}

The logic only moves the palette when it actually overlaps an active fold. I filter on isActive explicitly because Apple's overview says the method returns regions whether or not they're active, even though a separate .includeInactive option exists, and the filter makes the intent clear either way. For a fold that runs top to bottom, it centers the palette in whichever half is wider. For a fold that runs across the display, it lifts the palette so it sits just above the crease. Because the region IDs feed the animation, the palette slides to its new spot instead of jumping. In a real app you'd also want to handle a half that's narrower than the palette, for example by switching to a vertical palette, but the shape of the code stays the same.

Apple's Strike a pose with adaptive layouts on iPhone Duo tech talk calls this a displacement pattern, and the HIG spells out the rule behind it: move only what's necessary to keep things visible and easy to tap, and favor small adjustments over rearranging the screen. Controls that jump around dramatically as someone folds the device are harder to find, so resist the urge to rebuild the whole layout.

Inactive Regions and Cameras

By passing .includeInactive, you also get regions that exist but aren't active right now, such as the fold when the device is flat. That opens up a different approach. Instead of moving content when the fold appears, you can lay it out so nothing sits on the fold line in the first place. A grid with an even number of columns, which the HIG also recommends, gets you most of the way there, and the inactive fold's frame tells you exactly where the gutter needs to be.

Swift
let foldLines = proxy.reservedRegions(kind: .division, options: .includeInactive)
let gutterX = foldLines.first?.frame.midX

Occlusions work the same way with .occlusion as the kind. On the outer display, that's how a custom full-width header can find the camera in the corner and keep its trailing content clear of it. Each region's id stays stable across coordinate spaces and over time, so you can match a region you saw in one view against one you read somewhere else.

Reserved Regions in UIKit

UIKit has the same model as UIView.ReservedRegion. You ask a view for its regions with reservedRegions(kind:options:) and get frames in that view's coordinate space. Doing the check during layout keeps it next to the code that positions the view.

There's one catch. Folding the device doesn't change the view's bounds, and UIKit doesn't document a callback for when reserved regions change, so nothing guarantees a layout pass when the fold appears. A UIHingeInteraction fills that gap. Its handler runs whenever the hinge state changes, which is exactly when the fold becomes active or inactive, so it can invalidate layout:

Swift
final class SketchCanvasView: UIView {
    let palette = ToolPaletteView()
    private var lastHingeStatus: UIHinge.Status?

    override init(frame: CGRect) {
        super.init(frame: frame)
        addSubview(palette)

        addInteraction(UIHingeInteraction { [weak self] _, update in
            guard let self, update.hinge?.status != lastHingeStatus else { return }
            lastHingeStatus = update.hinge?.status
            setNeedsLayout()
        })
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        var paletteFrame = CGRect(
            x: (bounds.width - 280) / 2,
            y: bounds.height - 80,
            width: 280,
            height: 56
        )

        let folds = reservedRegions(kind: .division).filter(\.isActive)
        if let fold = folds.first(where: { $0.frame.intersects(paletteFrame) }),
           fold.frame.height > fold.frame.width {
            paletteFrame.origin.x = fold.frame.maxX + 16
        }

        palette.frame = paletteFrame
    }
}

Comparing against the last status keeps the handler from requesting a layout pass for every small change in angle. The layout code itself is simpler than the SwiftUI version only because it always moves the palette to the trailing side of a vertical fold. The same wider-half logic applies if you need it. For more on the hinge interaction, see Reading the iPhone Duo Hinge Angle.

Sample Project

Want to see this code in action? Check out the complete sample project on GitHub:

View on GitHub

The repository includes a working Xcode project with all the examples from this article, plus unit tests you can run to verify the behavior.

Wrapping Up

Reserved regions fill the gap between safe areas and full-blown layout containers. Use system views and ArrangementView wherever they fit, and when you're placing something yourself, ask the geometry proxy for .division or .occlusion regions, move only what overlaps, and keep the movement small. Then check the result in every pose in the Xcode 27.1 simulator. The iPhone Duo checklist covers the rest of what to test.

subscribe.sh

// Stay Updated

Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.

>

By subscribing, you agree to our Privacy Policy.