The iOS application uses two main components to allow navigation: NavigationStack and TabView. In this post, we’ll focus on the first one.
For simplicity, we’ll start by implementing navigation, moving from one view to an empty view.
The main component is the NavigationStack. You should define only one NavigationStack in the root view of your application. Otherwise, you will notice your screen height decrease because every NavigationStack adds a NavigationBar, which is bad for UX and can lead to memory leaks.
Let’s start by adding a link to an empty page:
struct ContentView: View {
var body: some View {
NavigationStack {
NavigationLink(destination: EmptyView()) {
Text("Go next")
}
}
}
}
Thus, by clicking on “Go Next,” you navigate to the empty page. We can declare the NavigationLink in different ways. The previous example is very common: we pass the destination view and define how the link is displayed in the body.
Another way is to use a label:
NavigationLink {
EmptyView()
} label: {
Label("Add Vehicle", systemImage: "plus")
}
So, we get something like this:
There are also other ways; you can refer to the official documentation for more details.
A common question I often receive is: how can I navigate to a different page after pressing a button?
There are two answers:
1. If you just need a NavigationLink with a button-like look and feel, you can customize the Text to look like a button.
2. If you need to perform an action (e.g., login) before navigating to another page, you can do it this way:
struct ContentView: View {
@State var login: Bool = false
var body: some View {
NavigationStack() {
Button("Login") {
login = true
}.navigationDestination(isPresented: $login) {
EmptyView()
}
}
}
}
So, we call navigationDestination, passing the login state variable. When this variable becomes true, the EmptyView is displayed (we’ll see a similar behavior with sheet).
Now, let’s look at a more realistic example: selecting a vehicle from a list and navigating to a detail page. We’ll start with this code:
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")]
@State private var searchText = ""
@State var login: Bool = false
var body: some View {
NavigationStack() {
List {
ForEach(searchResults) { vehicle in
NavigationLink(destination: EmptyView()) {
RowView(vehicle: vehicle)
}
}.onDelete { (indexSet) in
self.vehicles.remove(atOffsets: indexSet)
}
}.searchable(text: $searchText)
}
}
var searchResults: [Vehicle] {
if searchText.isEmpty {
return vehicles
} else {
return vehicles.filter {$0.name.lowercased().contains(searchText.lowercased())}
}
}
}
In this example, we navigate to an EmptyView, but the NavigationLink is displayed as a RowView:
struct RowView: View {
var vehicle: Vehicle
var body: some View {
HStack {
Image(systemName: vehicle.image)
.resizable()
.frame(width: 60, height: 60)
Text(vehicle.name)
}
}
}
Now, we want to display a DetailView. From the New menu, select New File from the template, choose SwiftUIView, and rename the new file to DetailView.
struct DetailView: View {
var body: some View {
Text("Hello, World!")
}
}
Now, we define a Vehicle model to use in the DetailView:
struct DetailView: View {
var vehicle: Vehicle
var body: some View {
VStack {
Image(systemName: vehicle.image)
.resizable()
.scaledToFit()
Text(vehicle.name)
.font(.largeTitle)
}.padding(20)
}
}
#Preview {
DetailView(vehicle: Vehicle(name: "car", image: "car"))
}
Note that in the preview, we pass a Vehicle; otherwise, an error will occur because the DetailView requires it.
Now, to use it, modify the NavigationLink in the ContentView as follows (1):
NavigationStack() {
List {
ForEach(searchResults) { vehicle in
NavigationLink(destination: DetailView(vehicle: vehicle)) { // 1
RowView(vehicle: vehicle)
}
}.onDelete { (indexSet) in
self.vehicles.remove(atOffsets: indexSet)
}
}.searchable(text: $searchText)
}
Good navigation.