Chapters

Hide chapters

Real-World iOS by Tutorials

First Edition · iOS 15 · Swift 5.5 · Xcode 13

Before You Begin

Section 0: 4 chapters
Show chapters Hide chapters

8. Navigation
Written by Aaqib Hussain

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

Previously, you learned to develop a framework and publish it as a Swift package. PetSave is taking shape, in the following chapters you’ll improve the user experience by introducing things like navigation and animations.

Navigation allows you to create experiences for your users. It covers how the user will navigate through the app and the different ways of getting the user from one place to another. One example is a feature you’ll work on in this chapter that allows users to get to a specific place in your app from a web browser.

Please note that this chapter is optional. If you would like to keep working on the final version of PetSave, feel free to move to the next chapter. Nonetheless, there’s a lot of useful information in this chapter that can help understand navigation not only on PetSave but in any app.

In this chapter, you’ll learn in detail about:

  • Navigation view

  • Types of navigation

  • Passing data between views

  • Navigating using a router

  • Navigate between SwiftUI and UIKit views

  • Presenting views

  • Tab view

You’ll learn how each of these components works and how to create navigation with them in different views.

It all starts with the navigation view.

Navigation view

NavigationView lets you arrange views in a navigation stack. Users can navigate to a destination view via a NavigationLink. The destination view is pushed into the stack. Whenever a user taps back or performs a swipe gesture, you can free up the stack by popping out the destination view.

You style the NavigationView with navigationViewStyle(_:). It currently supports DefaultNavigationViewStyle and StackNavigationViewStyle.

  • DefaultNavigationViewStyle: Use the navigation style of the current context where the view is presented.
  • StackNavigationViewStyle: A style where the view shows only a single top view at a given time.

Note: DoubleColumnNavigationViewStyle is now deprecated. iOS 15 comes with ColumnNavigationViewStyle to represent views in a column. This navigation style is more common in larger screen sizes like those on the bigger iPhones, iPads or a Mac.

You can create a custom style by implementing your own version of NavigationViewStyle or applying navigationTitle(_:) to customize the presented view’s appearance.

Navigation link

A NavigationLink is a view that controls a navigation presentation. It provides the view that will fire the navigation and present the destination.

var body: some View {
  // 1
  List {
  // 2
    ForEach(animals) { animal in
      // 3
      NavigationLink(destination: AnimalDetailsView()) {
        AnimalRow(animal: animal)
      }
    }

    footer
  } // 4
  .listStyle(.plain)
}
ForEach(animals) { animal in
  NavigationLink(
    animal.name ?? "",
    destination: AnimalDetailsView()
  )
}
Animals near you view with just animal names.
Oyijejf fiaj hiu viok quvq cawr eyewix fiwaw.

  @State var shouldShowDetails: Int? = -1
  var body: some View {
    List {
      ForEach(Array(animals.enumerated()), id: \.offset) { index, animal in
            NavigationLink(
              animal.name ?? "",
              destination: AnimalDetailsView(),
              tag: index,
              selection: $shouldShowDetails
            )
        }

      footer
    }
    .listStyle(.plain)
  }

Types of navigation

Navigation plays a vital role in giving the user a seamless experience. You must implement navigation so that the app works smoothly. Apple provides three styles of navigation:

Hierarchical navigation

In hierarchical navigation, the root view is the navigation view. You go from one screen to another. The navigation view pushes these screens into a navigation stack. You’ll find this navigation style in the Settings and Mail apps.

Ciovastlevoc doguwanouw raefnid.

Flat navigation

Flat navigation is usually a combination of TabView and NavigationView, which lets you switch between content categories. The Music and App Store apps are examples of such navigation.

Jzun dinehizuoz foozdoy.

Content-driven or experience-driven navigation

Content-driven or experience-driven navigation depends on the app’s content. Navigation may also depend on a user navigating to a particular screen. The Games and Books apps are examples of Content-Driven navigation.

Werlotq-psutes or Egyafoonso zivixadous liohzaz.

Passing data between views

There are four ways to pass data:

Using a property

Take it step by step. First, how do you use a property to pass data between views? You did that in earlier chapters, but you’ll revisit it now to understand better.

struct AnimalDetailsView: View {
  var name: String
  var body: some View {
    Text(name)
  }
}
NavigationLink(
  destination: AnimalDetailsView(
  name: animal.name ?? "")
){
   AnimalRow(animal: animal)
}

Using @State and @Binding

To keep both views, AnimalsNearYouView and AnimalDetailsView, up-to-date and reflecting proper data, you’ll need to manage the state. The sender view holds the data in a property marked with @State. The receiver receives the latest data with @Binding. This type of data passing assures both views stay updated. No matter where the data changes, both views get notified.

struct AnimalDetailsView: View {
  var name: String
  // 1
  @Binding var isNavigatingDisabled: Bool
  var body: some View {
    Text(name)
    // 2
    Button(isNavigatingDisabled ? "Enable Navigation" : "Disable Navigation") {
      isNavigatingDisabled.toggle()
    }
  }
}
AnimalDetailsView(name: "Snow", isNavigatingDisabled: .constant(false))
@State var isNavigatingDisabled = false
Button(isNavigatingDisabled ? "Enable Navigation" : "Disable Navigation") {
  isNavigatingDisabled.toggle()
}
ForEach(animals) { animal in
  // 1
  NavigationLink(
    destination: AnimalDetailsView(
      name: animal.name ?? "",
      isNavigatingDisabled: $isNavigatingDisabled
    )
  ) {
    AnimalRow(animal: animal)
  }
  .disabled(isNavigatingDisabled) // 2
}
Animals near you view enabled using @State and @Binding.
Ifafisg geak jou noek upegfib irepn @Sseqe ezk @Nofbill.

Animals near you view disabled using @State and @Binding.
Ozacipg faej fai miit kuxilnay odips @Nziba ijf @Jelnemm.

Animal details view using @State and @Binding.
Asoxaf siveuvz vaot upiqm @Vzeri akf @Necnicr.

Animals near you view disabled again using @State and @Binding.
Olikoqc feum bia daic runuhpob exook elunc @Vxuzu ejl @Totravx.

Using @StateObject and @ObservedObject

@StateObject holds the object responsible for updating the UI. You use it to refer to a class-type property in a view. You use the @ObservedObject property wrapper inside a view to store an observable object reference. Properties marked with @Published inside the observed object help the views change.

class NavigationState: ObservableObject {
  @Published var isNavigatingDisabled = false
}
@ObservedObject var navigationState: NavigationState
@Binding var isNavigatingDisabled: Bool
var body: some View {
  Text(name)
  Button(
    navigationState.isNavigatingDisabled ?
    "Enable Navigation" :
    "Disable Navigation"
  ) {
    navigationState.isNavigatingDisabled.toggle()
  }
}
AnimalDetailsView(
  name: "Snow",
  navigationState: NavigationState()
)
@StateObject var navigationState = NavigationState()
@State var isNavigatingDisabled = false
Button(
  navigationState.isNavigatingDisabled ?
  "Enable Navigation" : "Disable Navigation"
) {
  navigationState.isNavigatingDisabled.toggle()
}
ForEach(animals) { animal in
  NavigationLink(
    destination: AnimalDetailsView(
      name: animal.name ?? "",
      navigationState: navigationState
    )
  ) {
    AnimalRow(animal: animal)
  }
  .disabled(navigationState.isNavigatingDisabled)
}
Animals near you view enabled using @StateObject and @ObservedObject.
Idahalz vuip lie suur eromher eyalz @JfewaUtteyd ilk @UcsaxcabIrbocg.

Animal details view using @StateObject and @ObservedObject.
Upofuz pekeawc tuek exucm @NloduOhcurb uzt @EbgirmefAjsezw.

Using view’s environment

Environment objects can help you synchronize views. It catches the objects that are injected into the SwiftUI environment.

@EnvironmentObject var navigationState: NavigationState
AnimalDetailsView(name: "Snow").environmentObject(NavigationState())
@StateObject var navigationState = NavigationState()
ForEach(animals) { animal in
  NavigationLink(
    destination: AnimalDetailsView(name: animal.name ?? "")
    .environmentObject(navigationState)
  ) {
    AnimalRow(animal: animal)
  }
  .disabled(navigationState.isNavigatingDisabled)
}
Animals near you view enabled using @StateObject and @EnvironmentObject.
Unutonx taiz woi haet osufkuk umuyy @VcabeUvtihq epx @EktaluqsomyEyronb.

Animal details view using @StateObject and @EnvironmentObject.
Umasuh meguamt muot asagb @NrizoIbfedc ixj @OnpiqotdavcEyfagz.

Navigating using a router

Having multiple navigation links can make your view complex. You can decouple navigation links and make them more flexible by using a router. You’ll avoid nesting it inside the UI and therefore have more control over it. Having a router makes it easy to navigate and makes the UI agnostic of the navigation.

import SwiftUI
protocol NavigationRouter {
  // 1
  associatedtype Data
  // 2
  func navigate<T: View>(
    data: Data,
    navigationState: NavigationState,
    view: (() -> T)?
  ) -> AnyView
}
struct AnimalDetailsRouter: NavigationRouter {
  // 1
  typealias Data = AnimalEntity

  func navigate<T: View>(
    data: AnimalEntity,
    navigationState: NavigationState,
    view: (() -> T)?
  ) -> AnyView {
    AnyView( // 2
      NavigationLink(
        destination: AnimalDetailsView(name: data.name ?? "")
        .environmentObject(navigationState) // 3
      ) {
        view?()
      }
    )
  }
}
let router = AnimalDetailsRouter()
router.navigate(
  data: animal,
  navigationState: navigationState
) {
  AnimalRow(animal: animal)
}
.disabled(navigationState.isNavigatingDisabled)
Animals near you view enabled using navigation router.
Esegacy jues jeo miew awarhir itedt nuwifoziet niuveh.

Animal details view using navigation router.
Ubigap qexouns wian ihezq zizekitoar quobar.

Animals near you view disabled using navigation router.
Akucucd yuuc qau paep kuremdeq ikayg nadiqaquos ruehox.

Navigating using a router to a UIViewController

You learned how to use a router. Next, you’ll use it to navigate to an existing AnimalDetailsViewController.swift in UIKit.

xib file with a UILabel and a UIButton.
ruw yuye morm a OEPitit epq u EARimsif.

import UIKit
import SwiftUI

struct AnimalDetailsViewRepresentable: UIViewControllerRepresentable {
  // 1
  var name: String
  // 2
  @EnvironmentObject var navigationState: NavigationState
  // 3
  typealias UIViewControllerType = AnimalDetailsViewController
  // 4
  func updateUIViewController(
    _ uiViewController: AnimalDetailsViewController,
    context: Context) {
      // 5
      uiViewController.set(
        name,
        status: navigationState.isNavigatingDisabled
      )
      // 6
      uiViewController.didSelectNavigation = {
        navigationState.isNavigatingDisabled.toggle()
      }
  }
  // 7
  func makeUIViewController(context: Context)
    -> AnimalDetailsViewController {
      let detailViewController =
        AnimalDetailsViewController(
          nibName: "AnimalDetailsViewController",
          bundle: .main
        )
      return detailViewController
  }
}
func navigate<T: View>(
  data: AnimalEntity,
  navigationState: NavigationState,
  view: (() -> T)?
) -> AnyView {
  AnyView(
    NavigationLink(
      destination: AnimalDetailsViewRepresentable(
        name: data.name ?? ""
      ).environmentObject(navigationState)
    ) {
      view?()
    }
  )
}
Animal details view using UIViewControllerRepresentable.
Ogezex yafuaqn feal apahc AACionSiwdcuwziqLuqdiweyviwce.

Presenting views

SwiftUI provides you with two ways of presenting a view: Full screen cover and Sheet.

Full screen cover

You use full screen when you want to cover the entire screen and don’t want the user to swipe down to close the screen.

Sheet

Use a sheet when you want to let the user swipe the current view down to close it. This swiping to close feature is something you can disable as well.

ContentView().fullScreenCover(
  isPresented: $shouldPresentOnboarding,
  onDismiss: nil
)
ContentView().sheet(
  isPresented: $shouldPresentOnboarding,
  onDismiss: nil
)
Onboarding screens using a sheet view modifier.
Oynoeyvogr ndliosl ubeyj a ymaet siuj huroxaiw.

Using tab view

A tab view is a SwiftUI component that helps switch between multiple child views. It’s an example of flat navigation. If you have experience with UIKit, TabView is the SwiftUI version of UITabBarController.

var body: some View {
  TabView {
  // 1
    AnimalsNearYouView(
      viewModel: AnimalsNearYouViewModel(
        animalFetcher: FetchAnimalsService(
          requestManager:
            RequestManager()
        ),
        animalStore: AnimalStoreService(
          context: PersistenceController.shared.container.newBackgroundContext()
        )
      )
    )
    .tabItem {
      Label("Near you", systemImage: "location")
    }
    .environment(\.managedObjectContext, managedObjectContext)
  // 2
    SearchView()
      .tabItem {
        Label("Search", systemImage: "magnifyingglass")
      }
      .environment(\.managedObjectContext, managedObjectContext)
  }
}
.badge(2)
Near you tab with a badge of 2.
Kaux toi jeg fisb e weqfe eq 1.

enum PetSaveTabType {
  case nearYou
  case search
}
class PetSaveTabNavigator: ObservableObject {
  // 1
  @Published var currentTab: PetSaveTabType =  .nearYou
  // 2
  func switchTab(to tab: PetSaveTabType) {
    currentTab = tab
  }
}
// 3
extension PetSaveTabNavigator: Hashable {
  static func == (
    lhs: PetSaveTabNavigator,
    rhs: PetSaveTabNavigator
  ) -> Bool {
    lhs.currentTab == rhs.currentTab
  }

  func hash(into hasher: inout Hasher) {
    hasher.combine(currentTab)
  }
}
@StateObject var tabNavigator = PetSaveTabNavigator()
var body: some View {
// 1
  TabView(selection: $tabNavigator.currentTab) {
    AnimalsNearYouView(
      viewModel: AnimalsNearYouViewModel(
        animalFetcher: FetchAnimalsService(
          requestManager:
            RequestManager()
        ),
        animalStore: AnimalStoreService(
          context: PersistenceController.shared.container.newBackgroundContext()
        )
      )
    )
    .badge(2)
    // 2
    .tag(PetSaveTabType.nearYou)
    .tabItem {
      Label("Near you", systemImage: "location")
    }
    .environment(\.managedObjectContext, managedObjectContext)

    SearchView()
      .tag(PetSaveTabType.search) // 3
      .tabItem {
        Label("Search", systemImage: "magnifyingglass")
      }
      .environment(\.managedObjectContext, managedObjectContext)
  }
}

Deep link navigation with tab view

Now that you understand how to switch TabView programmatically. You’ll use this to navigate your way with a deep link.

Add URL Types.
Eqf ITV Jlxez.

Add URL Schemes.
Ozn ARX Zlgupav.

static func deepLinkType(url: URL) -> PetSaveTabType {
  if url.scheme == "petsave" {
    switch url.host {
    case "nearYou":
      return .nearYou
    case "search":
      return .search
    default:
      return .nearYou
    }
  }
  return .nearYou
}
// 1
.onOpenURL { url in    
  // 2
  let type = PetSaveTabType.deepLinkType(url: url)
  // 3
  self.tabNavigator.switchTab(to: type)
}
Deep link alert.
Yeuq midx oyihs.

PetSave's search opened using deep link.
VipCeci'z yientc oxorac ebucw faoq mocp.

PetSave's near you opened using deep link.
CodXiru'd puiw pei apuyak ekefs waoj zedx.

Key points

  • You can use a router to decouple the code and do navigation.
  • To make communication between SwiftUI and UIKit, you must implement UIViewControllerRepresentable.
  • To provide the user with a seamless experience, follow hierarchical, flat or content-driven navigation.
  • You can pass the view specific data using @State and @Binding.
  • You can pass custom data types using @StateObject and @ObservedObject.
  • You can create custom observable objects by conforming to ObservableObject. Make one of its properties a @Published so that it updates itself when that property changes.
  • You can use @Environment to read the system objects injected using .environment().
  • @EnvironmentObject can receive any object injected into the environment through .environmentObject(_).

Where to go from here?

That brings the end of this chapter. In this chapter, you went through various ways of performing navigation. Having a smooth navigational experience is something every user wants in an app. So when implementing navigation, you should always be mindful of that.

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