Windows and commands

Build a sidebar interface

Use NavigationSplitView to create a Mac-friendly sidebar, selection, and detail layout driven by stable identifiers.

12 minute lesson

~~~

Look at Mail, Notes, or Finder. The Mac pattern is the same everywhere: a sidebar list on the left, the selected item’s detail on the right. NavigationSplitView gives you that structure with the platform behavior users expect — a draggable divider, a toolbar button to collapse the sidebar, correct resizing.

On iOS you might reach for NavigationStack and push views. On the Mac, navigation is mostly selection: nothing gets pushed, the detail area shows whatever is selected. NavigationSplitView models exactly that.

Drive it with a selection binding that stores an identifier, not a whole note:

struct ContentView: View {
  @EnvironmentObject private var model: NotesModel
  @State private var selection: Note.ID?

  var body: some View {
    NavigationSplitView {
      List(model.notes, selection: $selection) { note in
        Text(note.title)
      }
      .navigationSplitViewColumnWidth(min: 180, ideal: 220)
    } detail: {
      NoteDetail(id: selection)
    }
  }
}

Because Note is Identifiable, the list tags each row with the note’s ID automatically. The detail view receives that ID and asks the model for the current data:

struct NoteDetail: View {
  @EnvironmentObject private var model: NotesModel
  let id: Note.ID?

  var body: some View {
    if let note = model.notes.first(where: { $0.id == id }) {
      Text(note.body)
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    } else {
      Text("Select a note")
        .foregroundStyle(.secondary)
    }
  }
}

Why an ID and not the note itself? The model stays the single source of truth. If the selection held a copy of the note, editing it elsewhere would leave the detail showing stale data. With an ID, the detail always resolves fresh state, and when the note is deleted the lookup fails cleanly instead of showing a ghost.

Run the app and check the behavior you got for free. Drag the divider. Click the sidebar toggle in the toolbar. Use the arrow keys in the list — selection follows the keyboard, which Mac users absolutely expect.

Then test the empty cases on purpose. Launch with no notes: the detail should show the placeholder, not crash. Select a note and delete it: selection now points at a note that no longer exists, the first(where:) lookup returns nil, and the placeholder comes back. Absence is a normal state for selection. Code that assumes “something is always selected” is the most common crash in this layout.

Lesson completed

Take this course offline

Get every free book and course as PDF and EPUB files.

Get the download library →