Chapters

Hide chapters

SwiftUI by Tutorials

Third Edition · iOS 14 · Swift 5.3 · Xcode 12

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

13. Navigation
Written by Bill Morefield

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

It’s a rare app the one that can work with only a single view; most apps use many views and provide a way for the user to navigate between them smoothly. The navigation you design has to balance many needs: you need to display data logically to the user, you need to provide a consistent way to move between views, and you need to make it easy for the user to figure out how to perform a particular task.

SwiftUI provides a unified interface to manage navigation while also displaying data. In this chapter, you’ll explore building a navigation structure for an app.

Getting started

Open the starter project for this chapter; you’ll find a very early version of an app for an airport. In this chapter, you will build out the navigation for this app. In a real-world app, you would likely get the flight information from an API through Combine. For this app, though, you’ll be using mock data.

Expand the Models folder in the app. Open FlightData.swift, and you’ll find the implementation of the mock data for this app. The FlightData class generates a schedule for fifteen days of flights with thirty flights per day starting with today’s date using the generateSchedule() method. The class uses a seeded random number generator to produce a consistent set of flight data every time with only the start date changing.

Also open and examine FlightInformation.swift, which encapsulates information about flights. You’ll be using this mock data through the next several chapters while building out this app.

Open WelcomeView.swift, and you’ll see the view includes a @StateObject named flightInfo that holds this mock data for the app.

Navigating through a SwiftUI app

When designing the navigation for your SwiftUI app, you must create a navigation pattern that helps the user move confidently through the app and intuitively perform tasks. Your users will rarely notice well-done navigation, but they won’t stand for an app that’s hard to navigate or makes it hard to find information. SwiftUI is a cross-platform framework but takes its primary design inspiration from iOS and iPadOS. Therefore, SwiftUI integrates patterns and design guidelines that are common on those platforms.

Flat navigation
Yxak pisoseviuc

Hierarchical navigation
Zeiwokljuqoy xolokufuuy

Creating navigation views

Build and run the starter app. You’ll see a bare-bones implementation with a graphic and a single option to view the day’s flight status board. In this chapter, you’ll change this view to use a hierarchical navigation with a NavigationView.

// 1
NavigationView {
  ZStack(alignment: .topLeading) {
    // 2
    Image("welcome-background")
      .resizable()
      .aspectRatio(contentMode: .fill)
      .frame(height: 250)
    VStack(alignment: .leading) {
      // 3
      NavigationLink(
        // 4
        destination: FlightStatusBoard()
      ) {
        // 5
        Text("Flight Status")
      }
      Spacer()
    }.font(.title)
    .foregroundColor(.white)
    .padding()
  // 6
  }.navigationBarTitle("Mountain Airport")
  // End Navigation View
}

.navigationViewStyle(StackNavigationViewStyle())

Sprucing up the links

Before moving to the child navigation views, you’ll improve the button’s look from the current plain text. Create a new SwiftUI View named WelcomeButtonView.swift. Replace the default view with the following:

struct WelcomeButtonView: View {
  var title: String
  var subTitle: String

  var body: some View {
    VStack(alignment: .leading) {
      Text(title)
        .font(.title)
        .foregroundColor(.white)
      Text(subTitle)
        .font(.subheadline)
        .foregroundColor(.white)
    }.padding()
    // 1
    .frame(maxWidth: .infinity, alignment: .leading)
    // 2
    .background(
      Image("link-pattern")
        .resizable()
        .clipped()
    )
  }
}
WelcomeButtonView(
  title: "Flight Status",
  subTitle: "Departure and Arrival Information"
)
WelcomeButtonView(
  title: "Flight Status",
  subTitle: "Departure and arrival information"
)

Using navigation links

You’ll first create a view that implements the first option from the Welcome view, providing more detailed information about today’s flight to the user.

var flights: [FlightInformation]
var body: some View {
  List(flights, id: \.id) { flight in
    Text(flight.statusBoardName)
  }.navigationBarTitle("Flight Status")
}
FlightStatusBoard(
  flights: FlightData.generateTestFlights(date: Date())
)
NavigationLink(
  destination: FlightStatusBoard(
    flights: flightInfo.getDaysFlights(Date()))
) {
  WelcomeButtonView(
    title: "Flight Status",
    subTitle: "Departure and arrival information"
  )
}
static var previews: some View {
  NavigationView {
    FlightStatusBoard(
      flights: FlightData.generateTestFlights(date: Date())
    )
  }
}

Extending the hierarchy

Your navigation follows the flow from more general information to more specific information. Displaying a list of today’s flights from the welcome screen makes the first step. Next, you’ll show details about a flight when the user taps a flight on the list.

List(flights, id: \.id) { flight in
  NavigationLink(
    flight.statusBoardName,
    destination: FlightDetails(flight: flight)
  )
}.navigationBarTitle("Flight Status")

Adding items to the navigation bar

Creating a navigation view stack adds a navigation bar to each view. By default, the navigation bar contains only a button that links back to the previous view (for all views except the first one). Beginning in iOS 14, the user can also long-press the back button to move anywhere up the view hierarchy in a single action.

@State private var hidePast = false
var shownFlights: [FlightInformation] {
  hidePast ?
    flights.filter { $0.localTime >= Date() } :
    flights
}
List(shownFlights, id: \.id) { flight in
.navigationBarItems(
  trailing: Toggle("Hide Past", isOn: $hidePast)
)

Navigation via code

The default navigation link responds to a user’s action, turning the view into a button. When the user taps that button, the movement to the next view triggers. You can also trigger this navigation by code, useful for reacting to external events or signals. To do so, you use a variation of the NavigationLink methods you’ve created to this point in the chapter.

NavigationLink(
  destination: FlightDetails(flight: flightInfo.flights.first!),
  // 1
  isActive: $showNextFlight
  // 2
) { }
@State var showNextFlight = false
Button(action: {
  showNextFlight = true
}) {
  WelcomeButtonView(
    title: "First Flight",
    subTitle: "Detail for First Flight of the Day"
  )
}

Sharing the environment

As you saw earlier, it’s simple to pass data down the navigation stack. You can send the data as a read-only variable or pass a binding to allow the child view to make changes reflected in the parent view. That works well for direct cases, but as the view hierarchy’s size and complexity increases, you’ll find that sending information back up can get complicated.

import SwiftUI

class FlightNavigationInfo: ObservableObject {
  @Published var lastFlightId: Int?
}
@StateObject var lastFlightInfo = FlightNavigationInfo()
.environmentObject(lastFlightInfo)
// 1
if
  let id = lastFlightInfo.lastFlightId,
  let lastFlight = flightInfo.getFlightById(id) {
  Button(action: {
    // 2
    showNextFlight = true
  }) {
    WelcomeButtonView(
    // 3
      title: "Last Flight \(lastFlight.flightName)",
      subTitle: "Show Next Flight Departing or Arriving at Airport"
    )
  }
}
if
  let id = lastFlightInfo.lastFlightId,
  let lastFlight = flightInfo.getFlightById(id) {
  NavigationLink(
    destination: FlightDetails(flight: lastFlight),
    isActive: $showNextFlight
  ) { }
}
@EnvironmentObject var lastFlightInfo: FlightNavigationInfo
.onAppear {
  lastFlightInfo.lastFlightId = flight.id
}
.environmentObject(FlightNavigationInfo())

Using tabbed navigation

You’ve been using and building a hierarchical view stack with NavigationView to this point in the app. Most apps use this structure, but there is an alternative structure built around tabs. Tabs work well for content where the user wants to flip between options. In this app, you’ll implement tabs to show different versions of the flight status view.

struct FlightList: View {
  var flights: [FlightInformation]

  var body: some View {
    List(flights, id: \.id) { flight in
      NavigationLink(
        flight.statusBoardName,
        destination: FlightDetails(flight: flight)
      )
    }
  }
}
// 1
TabView {
  // 2
  FlightList(
    flights: shownFlights.filter { $0.direction == .arrival }
  )
  // 3
  .tabItem {
    // 4
    Image("descending-airplane")
      .resizable()
    Text("Arrivals")
  }
  FlightList(
    flights: shownFlights
  )
  .tabItem {
    Image(systemName: "airplane")
      .resizable()
    Text("All")
  }
  FlightList(
    flights: shownFlights.filter { $0.direction == .departure }
  )
  .tabItem {
    Image("ascending-airplane")
    Text("Departures")
  }
}.navigationTitle("Flight Status")
.navigationBarItems(
  trailing: Toggle("Hide Past", isOn: $hidePast)
)

Setting tabs

It would be a nice addition to remember the last tab selected when the user returns to the view. Still in FlightStatusBoard.swift, below the hidePast state variable add the following line:

@AppStorage("FlightStatusCurrentTab") var selectedTab = 1
// 1
TabView(selection: $selectedTab) {
  FlightList(
    flights: shownFlights.filter { $0.direction == .arrival }
  ).tabItem {
    Image("descending-airplane")
      .resizable()
    Text("Arrivals")
    // 2
  }
  .tag(0)
  FlightList(
    flights: shownFlights
  ).tabItem {
    Image(systemName: "airplane")
      .resizable()
    Text("All")
  }
  .tag(1)
  FlightList(
    flights: shownFlights.filter { $0.direction == .departure }
  ).tabItem {
    Image("ascending-airplane")
    Text("Departures")
  }
  .tag(2)
}.navigationTitle("Flight Status")
.navigationBarItems(
  trailing: Toggle("Hide Past", isOn: $hidePast)
)

Key points

  • App navigation generally combines a mix of flat and hierarchical flows between views.
  • Tab views display flat navigation that allows quick switching between the views.
  • Navigation views create a hierarchy of views as a view stack. The user can move further into the stack and can back up from within the stack.
  • A navigation link connects a view to the next view in the view stack.
  • You should only have one NavigationView in a view stack. Views that follow should inherit the existing navigation view.
  • You apply changes to the navigation view stack to controls in the stack, and not to the NavigationView itself.

Where to go from here?

The first stop when looking for information on user interfaces on Apple platforms should be the Human Interface Guidelines on Navigation for iOS, watchOS and tvOS:

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now