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

iPhone Duo SwiftUI Navigation: Keep Selection and Drafts When the Display Changes

Updated
Read time
10 min read
Level
Intermediate
Platform
SwiftUI navigation, identifiable data, 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 NavigationSplitViewiPhone Duo app stateSwiftUI selection survives resizefoldable iPhone navigation

Quick Answer

Use one NavigationSplitView for your list-and-detail hierarchy, and keep selection and editable data above the layout that presents them. SwiftUI can collapse the columns on iPhone Duo's outer display and show them side by side on its inner display. The app's job is to keep the selected item, draft text, and pending work stable as the available scene size changes. Test opening, closing, rotation, and Split View while a real task is in progress, not just on an empty screen.

Requirements

  • Xcode 27.1 and the iPhone Duo simulator for Duo-specific validation.
  • SwiftUI with NavigationSplitView; target the minimum OS version your app supports for that API.
  • An item model with stable identifiers. The example below is in-memory so the layout boundary is clear; a shipped app needs persistence appropriate to its data.

Use This Pattern When

  • The same collection can show a compact list-first experience and a wide list-plus-detail experience.
  • A person may start editing on the outer display and continue after opening the device.
  • Your app can also appear in Duo's 50/50 Split View or resize beneath picture-in-picture.
  • The selected record has an identity independent of its row or view instance.

Avoid a split view merely to fill width. A single focused editor or immersive canvas may be better as one adaptive view. Apple's iPhone Duo design guidance says the hierarchy and available actions should remain consistent across poses.

Implementation

1. Own selection and edits outside the detail view

The trap is to create the draft inside a detail view that may be replaced when navigation collapses. Hold the source data and the selected ID in the parent. Resolve that ID against the current collection so deletion cannot leave a dangling detail.

import SwiftUI

struct Note: Identifiable, Hashable {
    let id: UUID
    var title: String
    var body: String
}

struct NotesWorkspace: View {
    @State private var notes: [Note] = [
        Note(id: UUID(), title: "Trip", body: "Pack the camera")
    ]
    @State private var selectedID: Note.ID?

    var body: some View {
        NavigationSplitView {
            List(notes, selection: $selectedID) { note in
                Text(note.title)
                    .tag(note.id)
            }
            .navigationTitle("Notes")
        } detail: {
            if let selectedID,
               let index = notes.firstIndex(where: { $0.id == selectedID }) {
                NoteEditor(note: $notes[index])
            } else {
                ContentUnavailableView(
                    "Select a note",
                    systemImage: "note.text"
                )
            }
        }
    }
}

struct NoteEditor: View {
    @Binding var note: Note

    var body: some View {
        Form {
            TextField("Title", text: $note.title)
            TextEditor(text: $note.body)
                .accessibilityLabel("Note body")
        }
        .navigationTitle(note.title)
    }
}

This example keeps every keystroke in parent state. For real data, use a persistent store and decide when edits commit. If editing must be cancelable, hold a draft keyed by note.id in a model owned above the split view, then commit or discard explicitly. Do not treat a View initializer as a storage boundary.

2. Let the container change presentation

Do not switch between separate NavigationStack and NavigationSplitView trees based on UIDevice or screen dimensions. That can reset selection and recreate editors during a fold. Apple's Duo preparation talk recommends adaptive system containers; NavigationSplitView is built to collapse when space is constrained. Check that the collapsed path still reaches every detail and action.

If your app has multiple windows or scenes, keep scene-specific selection in the scene's model rather than a global singleton. Shared data and current navigation are different kinds of state. Persist a draft if it must survive app termination; keeping it in @State only covers view updates while that owner remains alive.

3. Make deletion and errors explicit

When the selected item is deleted, set selectedID = nil or select a neighboring item as an intentional product choice. If saving fails, leave the draft visible with a retry path. Layout adaptation should never be the event that silently discards work.

Testing

Write a small model test for the selection boundary: after a record is deleted, selection is either cleared or points to a valid record. Then run this device matrix in Xcode's Duo simulator:

  1. Select a note on the outer display, type a draft, open Duo, and confirm the same note and text remain.
  2. Repeat in reverse, including while the keyboard is open.
  3. Enter 50/50 Split View, resize if available, and ensure detail remains reachable.
  4. Rotate, present a sheet, and return; check focus, scroll position, and unsaved text.
  5. Delete the selected note from another scene or sync source; verify the empty or next-item state.

For a testable selection rule, extract the deletion behavior into a small pure function:

func validSelection<ID: Equatable>(_ selected: ID?, in ids: [ID]) -> ID? {
    guard let selected, ids.contains(selected) else { return nil }
    return selected
}

// Swift Testing
import Testing

@Test func deletedSelectionClears() {
    #expect(validSelection(2, in: [1, 3]) == nil)
    #expect(validSelection(3, in: [1, 3]) == 3)
}

Common Mistakes

  • Branching on the device model: the same Duo can show several usable widths.
  • Giving list rows transient IDs, which makes selection unstable after refresh or sorting.
  • Copying the selected record into independent detail @State, so edits and source data diverge.
  • Rebuilding separate navigation trees on each width change, losing path and draft state.
  • Assuming an inner display is always full width; Split View and picture-in-picture can resize the scene.
  • Testing only open and closed poses, never a transition during editing.

Production Checklist

  • [ ] Every list item has a stable ID across launches and sync.
  • [ ] Selection survives open/close, rotation, and Split View.
  • [ ] Draft persistence and cancel behavior are documented in the product flow.
  • [ ] Deleting the selected record resolves to a valid UI state.
  • [ ] VoiceOver reaches the same controls in compact and expanded layouts.
  • [ ] Dynamic Type does not hide the editor or its save action.
  • [ ] A failed save keeps the user's text and offers recovery.
  • [ ] Large collections remain responsive during resizing.
  • [ ] Store screenshots show the actual layouts you ship.

Related

References

Authoritative References