Navigation in SwiftUI

Updated to iOS 18

In the previous posts, we have seen the fundaments of the SwiftUI. Now is the moment to starts to build something a bit more complex, how to implement the navigation in our apps with NavigationBar and the TabView.

NavigationBar

Start from the code of the previous post (https://nicoladefilippo.com/list-in-swiftui/):

struct Vehicle: Identifiable {
    var id = UUID()
    var name: String
    var image: String
}

struct ContentView: View {
    @State var vehicles = [Vehicle(name: "car", image: "car"),
                    Vehicle(name: "bus", image: "bus"),
                    Vehicle(name: "tram", image: "tram"),
                    Vehicle(name: "bicycle", image: "bicycle")]
    
    var body: some View {
        List {
            ForEach(vehicles) { vehicle in
                RowView(vehicle: vehicle)
            }
            .onDelete { (indexSet) in
                self.vehicles.remove(atOffsets: indexSet)
            }
        }
    }
}

struct RowView: View {
    var vehicle: Vehicle
    var body: some View {
        HStack {
            Image(systemName: vehicle.image)
                .resizable()
                .frame(width: 60, height: 60)
            Text(vehicle.name)
        }
    }
}

We want to tap on one row and skip to a new view.

The first step is add a NavigationStack:

var body: some View {
        NavigationStack {
            List {
                ForEach(vehicles) { vehicle in
                    NavigationLink { EmptyView()
                    } label:{
                        RowView(vehicle: vehicle)
                    }
                }
                .onDelete { (indexSet) in
                    self.vehicles.remove(atOffsets: indexSet)
                }
            }.navigationTitle("Transports")
        }
    }

In the NavigationStack we insert the List how in the previous example. To add a title to the NavigationStack, you have to use the navigationTitle function.

To jump to another view we have to use the NavigationLink. In the body of this, we insert the RowView (we want to click on the row to jump) and how destination we use EmptyView, a blank default view, so if we tap on any row:

We want a not empty view but a view that display something.

Let’s go to create a view. First, from the file menu select the new file voice and then:

From the dialog select SwiftUI View and call it VehicleView.

Replace the EmptyView with the VehicleView:

NavigationLink { 
      VehicleView(vehicle: vehicle)
    } label:{
         RowView(vehicle: vehicle)
    }

Now tapping on the list you jump on the new view and you’ll see “hello world”.

To show the info of the selected row, change the code of the VehicleView in:

import SwiftUI

struct VehicleView: View {
    var vehicle: Vehicle
    var body: some View {
        VStack(spacing: 20) {
            Image(systemName: vehicle.image)
                .resizable()
                .frame(width: 300, height: 300)
            Text("Selected the \(vehicle.name.uppercased())")
                .font(.title)
            Spacer()
        }
    }
}

#Preview {
    VehicleView(vehicle: Vehicle(name: "car", image: "car"))
}

In the view, there is defined a var of Vehicle type that we suppose contains the row passed, and show the information how defined in the VStack (see the post about VStack https://nicoladefilippo.com/layout-in-swiftui/). Note that in the Preview we pass a vehicle, it’s a default view used only for the preview (it’s helpful when you “design” the view without running the code).

How you can see we pass to the VehicleView vehicle. If everything it’s ok, if you tap the bus, you should have:

Is possible to have the title of the bar in the centre? Sure, add the navigationBarTitleDisplayMode in this way:

var body: some View {
        NavigationView {
            List {
                ForEach(vehicles) { vehicle in
                    NavigationLink(destination: VehicleView(vehicle: vehicle)) {
                        RowView(vehicle: vehicle)
                    }
                }
                .onDelete { (indexSet) in
                    self.vehicles.remove(atOffsets: indexSet)
                }
            }.navigationBarTitleDisplayMode(.inline)
                .navigationTitle("Vehicle")
        }
    }

Note the displayMode set to .inline.

Tab View

Now see how to define the TabView:

struct ContentView: View {
    var body: some View {
        TabView {
            Tab("Vehicles", systemImage: "list.dash") {
                Text("Tab1")
            }
            Tab("Profile", systemImage: "person.circle") {
                Text("Tab2")
            }
        }
    }
}

The magic word is TabView. In the body of this, we insert the views with relative Tab.

Now to create something more complex, we can use the code from the navigation stack example.

First, create a VehiclesView (note the plural).

import SwiftUI

struct Vehicle: Identifiable {
    var id = UUID()
    var name: String
    var image: String
}

struct VehiclesView: View {
    @State var vehicles = [Vehicle(name: "car", image: "car"),
                    Vehicle(name: "bus", image: "bus"),
                    Vehicle(name: "tram", image: "tram"),
                    Vehicle(name: "bicycle", image: "bicycle")]
    
    var body: some View {
        NavigationView {
            List {
                ForEach(vehicles) { vehicle in
                    NavigationLink(destination: VehicleView(vehicle: vehicle)) {
                        RowView(vehicle: vehicle)
                    }
                }
                .onDelete { (indexSet) in
                    self.vehicles.remove(atOffsets: indexSet)
                }
            }.navigationBarTitleDisplayMode(.inline)
                .navigationTitle("Vehicle")
        }
    }
}

struct RowView: View {
    var vehicle: Vehicle
    var body: some View {
        HStack {
            Image(systemName: vehicle.image)
                .resizable()
                .frame(width: 60, height: 60)
            Text(vehicle.name)
        }
    }
}

#Preview {
    VehiclesView()
}

Then, create a VehicleView as in the NavigationStack example and then change the TabView:

struct ContentView: View {
    var body: some View {
        TabView{
            Tab("Vehicles", systemImage: "list.dash") {
                VehiclesView()
            }
            Tab("Profile", systemImage: "person.circle") {
                Text("Tab2")
            }
        }
    }
}

Now in the first tab, we have a navigation stack and we can navigate in the first tab.

For exercise, you can create a profile page.

Note: English is not my native language, so I’m sorry for some errors. I appreciate it if your correct me.

share this post with friends

Picture of Nicola De filippo

Nicola De filippo

I'm a software engineer who adds to the passion for technologies the wisdom and the experience without losing the wonder for the world. I love to create new projects and to help people and teams to improve

2 comments

Leave a comment

Your email address will not be published. Required fields are marked *

Who I am

I'm a software engineer who adds to the passion for technologies the wisdom and the experience without losing the wonder for the world. I love to create new projects and to help people and teams to improve.

Follow Me Here

Get The Latest Updates

Periodically receive my super contents on coding and programming

join the family;)

Recent Posts