Navigation

Last updated on 7 Apr 2025.

Scroll Down

On this page

On this page

About NavigationStack

NavigationStack allows you to navigate through and present a stack of views over the root view. Views can be added to the top of the stack through multiple ways, and removed using controls like a Back button or a swipe gesture.

Navigating between views

NavigationLink

To navigate to another view, you can make use of a NavigationLink. In the example below, a NavigationLink is being used to open InformationView from PeopleView. A NavigationLink can only be used within a NavigationStack or NavigationSplitView.

struct PeopleView: View {
    let people: [Person] = [
        Person(name: "Tristan", height: 160, favColor: .blue),
        Person(name: "Jia Chen", height: 175, favColor: .red),
        Person(name: "Sean", height: 200, favColor: .green),
        Person(name: "YJ", height: 250, favColor: .yellow)
    ]

    var body: some View {
        NavigationStack {
            List {
                ForEach(people, id: \.self) { person in
                    NavigationLink {
                        InformationView(person: person)
                    } label: {
                        Text(person.name)
                    }
                }
            }
        }
    }
}

struct InformationView: View {
    let person: Person

    var body: some View {
        Text("\(person.name) is \(person.height)cm tall.")
            .font(.largeTitle)
            .foregroundStyle(person.favColor)
    }
}

.navigationDestination

The .navigationDestination(isPresented: destination:) modifier's syntax is similar to that of a .sheet, where you can programmatically display and remove a view by changing the value of the isPresented argument.

The example code below describes how a settings page can be opened programmatically either via a button in the view or a button in the toolbar.

@State private var showingSettings: Bool = false
NavigationStack {
    List {
        Section {
            Button("Show Settings") {
                showingSettings.toggle()
            }
         }
     }
     .toolbar {
         ToolbarItem(placement: .topBarTrailing) {
             Button {
                 showingSettings.toggle()
            } label: {
                Label("Show Settings", systemImage: "gear")
            }
        }
    }
    .navigationDestination(isPresented: $showingSettings) {
        Text("Modify settings here.")
    }
}

The .navigationDestination(item: destination:) modifier can be used to add a view to the top of the stack by assigning the item argument with a value. This modifier is useful when you want to keep track of the option the user has selected, while navigating to another view. You can also programmatically update the binding to remove the view by setting the value of the variable to nil.

The example below shows how the modifier can be used to programmatically add a view to the stack, keeping track of which person (if any) is being selected and printing the result into the console. .navigationDestination(item: destination:) can only be used within a NavigationStack or NavigationSplitView.

@State private var selectedPerson: Person
NavigationStack {
    List {
        ForEach(people, id: \.self) { person in
            Button(person.name) { selectedPerson = person }
        }
    }
    .navigationDestination(item: $selectedPerson) { person in
        InformationView(person: person)
    }
    .onChange(of: selectedPerson) {
        print(selectedPerson)
    }
}

The .navigationDestination(for: destination:) modifier can be used to show a view for a specific data type. The example below shows how the modifier can be used, and how different data types open different views. .navigationDestination(for: destination:) can only be used within a NavigationStack or NavigationSplitView.

NavigationStack {
    List {
        Section {
            ForEach(people, id: \.self) { person in
                NavigationLink(person.name, value: person)
            }
        }

        Section {
            ForEach(people, id: \.self) { person in
                NavigationLink(person.name, value: person.name)
            }
        }
    }
    .navigationDestination(for: Person.self) { person in
        Text("\(person.name) is \(person.height)cm tall.")
            .font(.largeTitle)
            .foregroundStyle(person.favColor)
    }
    .navigationDestination(for: String.self) { name in
        Text("The person's name is: \(name)")
            .font(.largeTitle)
    }
}

More NavigationStack

Paths

You can control and keep track of views in the a NavigationStack by creating a Binding value and passing it into the path argument, which is of type NavigationPath. NavigationPath accepts all data types, and even mixes of them, as long as they conform to the Hashable protocol. You can append to the value of the path argument by using the .append method to add a view to the stack. You can also use the .removeLast method to remove the last view from the stack, which would also be the current view on the stack. Passing an Int as an argument into the .removeLast() argument removes the last n number of views from the stack, and assigning the value of the path argument as NavigationPath clears out the whole stack.

The example below shows how you can append views to the stack using NavigationPath, remove a specific number of views from the stack, and remove every single view from the stack.

@State private var path = NavigationPath() // Creates an empty path
NavigationStack(path: $path) {
    List {
        Button("Add a Jia Chen") {
            path.append(Person(name: "Jia Chen", height: 175, favColor: .red))
        }

        Button("Add 5 Hellos") {
            for i in 1..<6 {
                path.append("Hello \(i)")
            }
        }
    }
    .navigationDestination(for: Person.self) { person in
        Text("\(person.name) is \(person.height)cm tall.")
            .font(.largeTitle)
            .foregroundStyle(person.favColor)
    }
    .navigationDestination(for: String.self) { string in
        Text(string)
        Button("Remove last 2 Views from stack") { path.removeLast(2) } // Removes last 2 views from stack
        Button("Clear stack") { path = NavigationPath() } // Removes every view from stack
    }
}

.navigationTitle

Make use of .navigationTitle to create a title for your views. In the example below, a title is used to provide more context on the current view you are in. .navigationTitle can only be used within a NavigationStack or NavigationSplitView, or within a view that is a part of a NavigationStack's stack, without it needing to be wrapped within a NavigationStack itself.

struct PeopleView: View {
    ...

    var body: some View {
        NavigationStack {
            List {
                ForEach(people, id: \.self) { person in
                    NavigationLink(person.name, destination: InformationView(person: person))
                }
            }
            .navigationTitle("People")
        }
    }
}

struct InformationView: View {
    let person: Person

    var body: some View {
        VStack {
            Text("\(person.name) is \(person.height)cm tall.")
                .font(.largeTitle)
                .foregroundStyle(person.favColor)
        }
        .navigationTitle("Information")
    }
}

.navigationBarTitleDisplayMode

You can also make your title smaller and more space-efficient by using the .navigationBarTitleDisplayMode(.inline) modifier

struct InformationView: View {
    let person: Person

    var body: some View {
        VStack {
            Text("\(person.name) is \(person.height)cm tall.")
                .font(.largeTitle)
                .foregroundStyle(person.favColor)
        }
        .navigationTitle("Information about \(person.name)")
        .navigationBarTitleDisplayMode(.inline)
    }
}

NavigationSplitView

NavigationSplitView allows you to create two or three column views, with each leading column controlling the view in the subsequent column.

The example below shows a two column NavigationSplitView. The leading column called the sidebar, is being used to control what is being presented in the trailing column, the detail.

@State private var selectedBatch: Batch?
@State private var selectedPerson: Person

NavigationSplitView {
    List(batches, selection: $selectedBatch) { batch in
        Text(String(batch.year))
            .tag(batch)
    }
    .navigationTitle("Batch")
} content: {
    if let batch = selectedBatch {
        List(batch.people) { person in
            Text(person.name)
        }
        .navigationTitle("People")
    } else {
        Text("Select a batch.")
    }
}

As shown in the example below, to create a three column NavigationSplitView, add the content argument before detail. In this case, the contents within the sidebar column will control the contents within the content column, which will then control the contents within the detail column.

NavigationSplitView {
    List(batches, selection: $selectedBatch) { batch in
        Text(String(batch.year))
            .tag(batch)
    }
    .navigationTitle("Batch")
} content: {
    if let batch = selectedBatch {
        List(batch.people, selection: $selectedPerson) { person in
            Text(person.name)
                .tag(person)
        }
        .navigationTitle("People")
    } else {
        Text("Select a batch.")
    }
} detail: {
    if let person = selectedPerson {
        List {
            LabeledContent("Name", value: person.name)
            LabeledContent("Height", value: String(person.height))
            LabeledContent("Favourite Color") {
                Circle()
                    .foregroundStyle(person.favColor)
                    .frame(width: 16, height: 16)
            }
        }
        .navigationTitle("Information")
    } else {
        Text("Select a person.")
    }
}

Modifying Columns

You're able to programmatically control the visibility of columns in a NavigationSplitView by creating a State variable of type NavigationSplitViewVisibility and passing that into the preferredColumn: argument.

The example below shows a column visibility of .detailOnly.

@State private var columnVisibility: NavigationSplitViewVisibility = .detailOnly

The example below shows a column visibility of .doubleColumn.

The example below shows a column visibility of .all.

Preferred Columns

You can set a preferred column when the app becomes compact using the preferredCompactColumn: argument. Create a state variable of type NavigationSplitViewColumn and pass it into the argument. For example, if your app is running on an iPhone or if your iPad app is being used in in multitasking, the first column that is shown will be the column specified in your preferredCompactColumn argument.

Following the example code below, the detail column will be shown first if the app's size becomes compact.

@State private var preferredColumn: NavigationSplitViewColumn = .detail
NavigationSplitView(preferredCompactColumn: $preferredColumn) {
    ...
} content: {
    ...
} detail: {
    ...
}

Styling

Width

You're able to specify the width of each of the columns in a NavigationSplitView using the .navigationSplitViewColumnWidth(_:) modifier. To set minimum, maximum, and ideal sizes for a column, use the .navigationSplitViewColumnWidth(min:ideal:max:). However, NavigationSplitView may still make adjustments to the width of the columns depending on the constraints of the view.

Below shows an example of a sidebar with a .navigationSplitViewColumnWidth(200)modifier.

NavigationSplitView {
    ...
    .navigationSplitViewColumnWidth(200)
} content: {
    ...
} detail: {
    ...
}

NavigationSplitViewStyle

You're able to customise the style of your NavigationSplitView using the .navigationSplitViewStyle modifier.

The example below shows the .prominentDetail style, where the sidebar and content are layered above the detail view area.

NavigationSplitView {
    ...
} content: {
    ...
} detail: {
    ...
}
.navigationSplitViewStyle(.prominentDetail)

In These Collections

This article can also be found in these collections.

© 2025 Tinkertanker Pte Ltd / Swift Accelerator. All rights reserved.

© 2025 Tinkertanker Pte Ltd / Swift Accelerator. All rights reserved.

© 2025 Tinkertanker Pte Ltd / Swift Accelerator. All rights reserved.