Skip to main content
3Nsofts logo3Nsofts
Navigation & App StructureUpdated ·

iPhone Duo SwiftUI Toolbars: Side Bars, Safe Areas, and Fold-Safe Controls

Updated
Read time
11 min read
Level
Intermediate
Platform
SwiftUI NavigationStack or NavigationSplitView, Xcode 27.1

Implementation Notes

  • ~/ What broke: A production edge case that generic tutorials skip.
  • ~/ What to do: Ship the production fix with clear state, errors, and fallback behavior.
iPhone Duo SwiftUI toolbariPhone Duo vertical bariPhone Duo safe areaiPhone Duo fold controls

Quick Answer

Put actions in SwiftUI's toolbar within NavigationStack or NavigationSplitView, and let iPhone Duo move eligible system bars to the display edge. Keep foreground controls inside safe areas; let decorative backgrounds extend farther. Audit actions that move into overflow and any custom controls near the fold. Use reserved-region geometry only for a custom layout that system containers cannot handle.

Requirements

  • Xcode 27.1 and iPhone Duo simulator for the latest Duo bar behavior.
  • SwiftUI and a navigation container for native toolbar placement.
  • iOS 27.1 for the new reserved-region APIs discussed below. The basic toolbar and safe-area pattern also helps on older supported iOS versions.

Use This Pattern When

  • Your app already has navigation, tabs, sheets, or toolbars and you want them to adapt to Duo.
  • A custom floating button or inspector risks landing beneath the side bar or fold.
  • A wide label, segmented control, or destructive action becomes hard to find in a vertical bar.
  • Your UI is shown on the outer display, inner display, and in Split View.

Avoid drawing a new bar just to mimic Duo's system bar. Apple's bar guidance specifically recommends navigation-container bars so the system can place and manage them.

Implementation

1. Start with a real navigation container and toolbar

import SwiftUI

struct ReadingScreen: View {
    @State private var showingSettings = false
    @State private var showingLibrary = false
    @State private var isSaved = false

    var body: some View {
        NavigationStack {
            ScrollView {
                Text("Article text goes here.")
                    .frame(maxWidth: 720, alignment: .leading)
                    .frame(maxWidth: .infinity)
                    .padding()
            }
            .navigationTitle("Reading")
            .toolbar {
                ToolbarItem(placement: .topBarTrailing) {
                    Button {
                        isSaved.toggle()
                    } label: {
                        Label(
                            isSaved ? "Remove bookmark" : "Bookmark",
                            systemImage: isSaved ? "bookmark.fill" : "bookmark"
                        )
                    }
                }
                ToolbarItem(placement: .topBarTrailing) {
                    Button("Settings", systemImage: "gearshape") {
                        showingSettings = true
                    }
                }
            }
            .sheet(isPresented: $showingSettings) {
                NavigationStack {
                    Text("Reading settings")
                        .navigationTitle("Settings")
                        .toolbar {
                            ToolbarItem(placement: .confirmationAction) {
                                Button("Done") { showingSettings = false }
                            }
                        }
                }
            }
        }
    }
}

With a current SDK, the system can place eligible bars vertically on Duo's outer display and inner landscape display. Inner portrait returns to horizontal bars. Do not hard-code an edge or rotate the toolbar yourself. Buttons need short, distinct labels and accessible names because space is shared with navigation, tabs, status elements, and Live Activities. Apple notes that lower-priority items may enter an overflow menu.

2. Separate decorative bleed from tappable foreground

A background can reach an edge; text and buttons must remain usable within safe areas. Avoid ignoresSafeArea() on an entire interactive screen. Apply it to a background only:

ZStack {
    Color.indigo.ignoresSafeArea()

    VStack {
        Text("Ready to read")
            .font(.largeTitle)
        Button("Open library") { showingLibrary = true }
    }
    .padding()
}

This is more important on Duo because safe areas can be asymmetric around edge controls and cameras. A centered view based on the full display bounds can look visually off-center or become partly covered. Test with large text and right-to-left layout direction too; the physical bar edge does not necessarily mirror with language direction.

3. Query reserved regions only for bespoke controls

For a custom control that still intersects the fold after respecting safe areas, iOS 27.1 exposes reserved regions through GeometryProxy. The fold is a division region; cameras are occlusion regions. Use the returned frames to decide where to place your control in your own coordinate space. The minimal query is:

GeometryReader { proxy in
    let foldFrames = proxy.reservedRegions(kind: .division).map(\.frame)

    Text("Canvas with \(foldFrames.count) active division regions")
}

The query does not move your view for you. For most apps, a system List, ScrollView, sheet, alert, menu, or navigation container is preferable: Apple says these already adapt around the fold. Continuous reading content should usually remain scrollable instead of being displaced to another region. Reach for a custom displacement only for a bounded element such as playback controls or a floating confirmation button. See Apple's adaptive-layout talk for ArrangementView when two custom views need to split or overlay across poses.

Testing

Build against the current SDK, then test on Duo's outer display, inner landscape, inner portrait, and partially folded poses. Repeat in Split View and with picture-in-picture. Open every toolbar item, sheet, alert, and overflow menu. Confirm that a primary action remains discoverable when bars compress and that no custom hit target crosses a fold or camera occlusion.

For a custom placement policy, keep the choice separate from the view and unit-test it:

func needsDisplacement(control: CGRect, reserved: [CGRect]) -> Bool {
    reserved.contains { $0.intersects(control) }
}

import Testing
import CoreGraphics

@Test func foldIntersectionRequiresDisplacement() {
    let fold = CGRect(x: 200, y: 0, width: 12, height: 400)
    #expect(needsDisplacement(
        control: CGRect(x: 195, y: 90, width: 44, height: 44),
        reserved: [fold]
    ))
}

Use this only after converting both rectangles into the same coordinate space. A passing rectangle test cannot replace a device interaction test.

Common Mistakes

  • Building a lookalike side rail instead of using toolbar inside a navigation container.
  • Assuming every action remains visible when the bar competes with Live Activities and tabs.
  • Calling ignoresSafeArea() on controls rather than only on a background.
  • Positioning custom controls from UIScreen.main.bounds instead of the scene's available geometry.
  • Moving a whole scrolling article away from the fold, causing disruptive jumps.
  • Checking only left-to-right text and default Dynamic Type size.

Production Checklist

  • [ ] Primary actions stay reachable in all Duo poses and overflow states.
  • [ ] Sheets and alerts use native presentation unless a clear product need prevents it.
  • [ ] Touch targets do not overlap a fold, camera, or bar.
  • [ ] VoiceOver reads each icon-only action with an unambiguous label.
  • [ ] Dynamic Type and right-to-left layouts remain usable.
  • [ ] Custom geometry handles active and absent reserved regions.
  • [ ] Resizing does not trigger expensive repeated layout work or visual jumps.
  • [ ] Sensitive content stays out of screenshots and logs used for layout QA.
  • [ ] App Store imagery accurately reflects the shipped interface.

Related

References

Authoritative References