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

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.

NavigationLink can reside directly inside a NavigationView. It’s commonly used with a List or Button or on some action performed by the user.

Open AnimalListView.swift and take a look at body:

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

    footer
  } // 4
  .listStyle(.plain)
}

Here’s a code breakdown:

  1. The top-level view is List.
  2. List nests a ForEach to render the animals.
  3. NavigationLink takes in the destination view you want to show. When the user taps this view, the destination view gets nested inside the current view.
  4. The list style is plain.

To render only the text that the user taps to go to the destination view, use a convenience initializer that takes in a string and creates the Text view. Replace the entire ForEach with:

ForEach(animals) { animal in
  NavigationLink(
    animal.name ?? "",
    destination: AnimalDetailsView()
  )
}

Note: In Xcode, double-click any curly opening brace to select the entire code block within the curly opening and closing braces. You can also use Command-/ to comment the entire code block or press delete to delete the entire code block.

Build and run. You’ll see this render:

Animals near you view with just animal names.
Animals near you view with just animal names.

Alternatively, you can do navigation programmatically, using the navigation link like this:

  @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)
  }

You use the navigation link’s initializer with selection and tag parameters. Here, the navigation view presents the destination view when the index matches the bound property shouldShowDetails.

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
  • Flat Navigation
  • Content-Driven or Experience-Driven 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.

Hierarchical navigation diagram.

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.

Flat navigation diagram.

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.

Content-driven or Experience navigation diagram.

It’s not written in stone that you can only use one of these navigation styles in a given instance. For example, you may combine flat with hierarchical. Many apps use multiple navigation styles.

However, it’s important to always give the user a clear path for navigation. Users must know where they are in your app at all times. Make sure you provide users with the minimum number of taps or swaps to reach their destination.

Passing data between views

There are four ways to pass data:

  • Use a property.
  • Use@State and @Binding.
  • Use @StateObject and @ObservedObject.
  • Use view’s environment.

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.

Take a look at this code example:

struct AnimalDetailsView: View {
  var name: String
  var body: some View {
    Text(name)
  }
}

The name property is of type String and passes into the Text view.

Then, consider this:

NavigationLink(
  destination: AnimalDetailsView(
  name: animal.name ?? "")
){
   AnimalRow(animal: animal)
}

The AnimalDetailsView takes in the name of an animal object. If an animal isn’t given a name, it takes in a default empty string.

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.

To better understand this concept, you’ll make a small tweak to the code. You’ll add a button to the list to enable or disable the user interaction of the navigation link on AnimalsNearYouView, this time using @State and @Binding. You’ll add two buttons in the views AnimalListView and AnimalDetailsView to control the state from both views.

First, open AnimalDetailsView.swift and replace AnimalDetailsView implementation with:

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()
    }
  }
}
  1. You add the receiver view that gets the @Binding property wrapper to listen to/return the changes from/to the sender.
  2. A button to toggle the isNavigatingDisabled to either true or false.

Update your AnimalsView_Previews, so you pass the new parameter to AnimalDetailsView:

AnimalDetailsView(name: "Snow", isNavigatingDisabled: .constant(false))

Here you pass a constant to isNavigatingDisabled, so the view can be rendered in Xcode previews.

Now, open AnimalListView.swift and add a @State variable to the navigation state in the sender view:

@State var isNavigatingDisabled = false

Then, add a button nesting List before the ForEach:

Button(isNavigatingDisabled ? "Enable Navigation" : "Disable Navigation") {
  isNavigatingDisabled.toggle()
}

This button will toggle the state of the navigation.

Still in AnimalListView.swift replace the ForEach with:

ForEach(animals) { animal in
  // 1
  NavigationLink(
    destination: AnimalDetailsView(
      name: animal.name ?? "",
      isNavigatingDisabled: $isNavigatingDisabled
    )
  ) {
    AnimalRow(animal: animal)
  }
  .disabled(isNavigatingDisabled) // 2
}

Here, you are:

  1. Passing the navigation state to AnimalDetailsView.
  2. Depending on the state enabling or disabling the user interaction.

Build and run. You’ll see a view like this:

Animals near you view enabled using @State and @Binding.
Animals near you view enabled using @State and @Binding.

Tap Disable Navigation. You’ll see disabled navigation.

Animals near you view disabled using @State and @Binding.
Animals near you view disabled using @State and @Binding.

Similarly, enable navigation and select any animal. On AnimalDetailsView, you’ll see a Disable Navigation button.

Animal details view using @State and @Binding.
Animal details view using @State and @Binding.

Tap Disable Navigation and go back. You’ll see disabled navigation on AnimalsNearYouView like this:

Animals near you view disabled again using @State and @Binding.
Animals near you view disabled again using @State and @Binding.

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.

One difference between @State and @StateObject is that the first one works with value types and the latter with reference types.

Back in AnimalsNearYouView.swift, add the following code:

class NavigationState: ObservableObject {
  @Published var isNavigatingDisabled = false
}

With this, you created NavigationState which has one @Published property called isNavigatingDisabled. isNavigatingDisabled observes the value and maintains the state of the navigation view.

Now, open AnimalDetailsView.swift. Add a property navigationState with property wrapper @ObservedObject like this:

@ObservedObject var navigationState: NavigationState

Here using the @ObservedObject so the UI updates when the state changes. Also, remove the following property, as it’s no longer needed:

@Binding var isNavigatingDisabled: Bool

Then, update body to:

var body: some View {
  Text(name)
  Button(
    navigationState.isNavigatingDisabled ?
    "Enable Navigation" :
    "Disable Navigation"
  ) {
    navigationState.isNavigatingDisabled.toggle()
  }
}

Here, the button action uses the published property isNavigatingDisabled in NavigationState to toggle the state.

Then, in the preview code, update AnimalDetailsView to:

AnimalDetailsView(
  name: "Snow",
  navigationState: NavigationState()
)

This code updates the preview with the latest changes in AnimalDetailsView.

Open AnimalListView.swift and add:

@StateObject var navigationState = NavigationState()

@StateObject keeps the current state of the observable object.

Then, delete isNavigatingDisabled as it’s no longer in use.

@State var isNavigatingDisabled = false

Update the button in the List to:

Button(
  navigationState.isNavigatingDisabled ?
  "Enable Navigation" : "Disable Navigation"
) {
  navigationState.isNavigatingDisabled.toggle()
}

The button now uses the updated @StateObject.

Then, update ForEach in the List to:

ForEach(animals) { animal in
  NavigationLink(
    destination: AnimalDetailsView(
      name: animal.name ?? "",
      navigationState: navigationState
    )
  ) {
    AnimalRow(animal: animal)
  }
  .disabled(navigationState.isNavigatingDisabled)
}

You pass the state object to the AnimalDetailsView initializer.

Build and run.

Animals near you view enabled using @StateObject and @ObservedObject.
Animals near you view enabled using @StateObject and @ObservedObject.

Tap any animal to go to the animal’s details view.

Animal details view using @StateObject and @ObservedObject.
Animal details view using @StateObject and @ObservedObject.

You’ll find that everything works as before.

Using view’s environment

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

Back in AnimalDetailsView.swift, update the navigationState like this:

@EnvironmentObject var navigationState: NavigationState

This snippet captures the object that is injected into the environment.

Then, in the preview, update AnimalDetailsView to:

AnimalDetailsView(name: "Snow").environmentObject(NavigationState())

The preview is now in-sync with the latest changes made to the view.

In AnimalListView.swift, make sure the navigationState is a @StateObject:

@StateObject var navigationState = NavigationState()

Next, update the ForEach to:

ForEach(animals) { animal in
  NavigationLink(
    destination: AnimalDetailsView(name: animal.name ?? "")
    .environmentObject(navigationState)
  ) {
    AnimalRow(animal: animal)
  }
  .disabled(navigationState.isNavigatingDisabled)
}

Here, you inject the navigationState from the sender view in the environment.

Now, build and run.

Animals near you view enabled using @StateObject and @EnvironmentObject.
Animals near you view enabled using @StateObject and @EnvironmentObject.

Select an animal and go to the details view.

Animal details view using @StateObject and @EnvironmentObject.
Animal details view using @StateObject and @EnvironmentObject.

Play around with the app. Everything works smoothly as it previously did.

Now that you have a sense of how to pass around data. You’ll use that and learn another different way of doing navigation.

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.

You can customize the view a lot before navigating to that screen. A router provides a layer of abstraction. If you navigate to a UIViewController, your SwiftUI view won’t be affected in any way.

To gain a better understanding, try this quick exercise.

Under Core/utils, create a new file named NavigationRouter.swift and add:

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

Here’s a code breakdown:

  1. During the implementation of this protocol, you need to provide the data type you want to pass to the destination view.
  2. Calling this method inside the view with the appropriate data returns a destination view. It also requires you to pass the state.

Now open AnimalDetailsView.swift and add:

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?()
      }
    )
  }
}

AnimalDetailsRouter implements NavigationRouter with the implementation for navigating between views.

  1. Data type is AnimalEntity.
  2. This returns an AnyView within a NavigationLink with AnimalDetailsView as the destination view.
  3. Passing the navigationState towards the AnimalDetailsView.

Now picking up the previous example about enabling/disabling the user interaction on navigation using buttons in both views.

Back in AnimalListView.swift, add the following properties to the struct:

let router = AnimalDetailsRouter()

Here, you add a router object to initiate navigation.

Then inside the ForEach, replace NavigationLink with:

router.navigate(
  data: animal,
  navigationState: navigationState
) {
  AnimalRow(animal: animal)
}
.disabled(navigationState.isNavigatingDisabled)

The router returns a view nested with a navigation link and performs the required navigation.

Now, build and run. You’ll see:

Animals near you view enabled using navigation router.
Animals near you view enabled using navigation router.

Select an animal, and you’ll see the AnimalDetailsView:

Animal details view using navigation router.
Animal details view using navigation router.

Then, select the Disable Navigation button and go back to AnimalsNearYouView. You’ll see a disabled navigation.

Animals near you view disabled using navigation router.
Animals near you view disabled using navigation router.

It works as expected. That’s great!

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.

Open AnimalDetailsViewController.xib. You’ll see a screen with a UILabel and a UIButton.

xib file with a UILabel and a UIButton.
xib file with a UILabel and a UIButton.

To make the UIViewController talk to the SwiftUI view, you’ll implement a bridge between them. SwiftUI provides a protocol, UIViewControllerRepresentable, and requires you to provide the UIViewController to which you want to connect. You also need to provide a method to reflect the changes you want to make to this view controller.

Under AnimalDetails/views, create AnimalDetailsViewRepresentable.swift. Then add:

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
  }
}

Here’s what the code is doing:

  1. Creates a variable to receive the name of the animal from the AnimalsNearYouView.
  2. Gets the state of the navigation in the current environment.
  3. Assigns the type of the destination view controller. Here, it’s the AnimalDetailsViewController.
  4. This method is from the UIViewControllerRepresentable. Here, you update the changes coming from the SwiftUI view.
  5. You set the name of the animal and the status of the navigation here.
  6. This closure listens to the button inside the AnimalDetailsViewController and toggles the navigation state.
  7. This method is also from UIViewControllerRepresentable. Here, you return the view controller you want SwiftUI to make renderable.

SwiftUI requires both of these methods’ implementation to make and update the views.

Now, open AnimalDetailsView.swift. Then, in AnimalDetailsRouter, update navigate method as:

func navigate<T: View>(
  data: AnimalEntity,
  navigationState: NavigationState,
  view: (() -> T)?
) -> AnyView {
  AnyView(
    NavigationLink(
      destination: AnimalDetailsViewRepresentable(
        name: data.name ?? ""
      ).environmentObject(navigationState)
    ) {
      view?()
    }
  )
}

Here, you replace the AnimalDetailsView with the new representable view.

Now, build and run. Select an animal and go to the AnimalDetailsView. The navigation works fine.

Animal details view using UIViewControllerRepresentable.
Animal details view using UIViewControllerRepresentable.

You can’t even tell the difference in the look and feel, right? That’s how cool SwiftUI is.

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.

Open AppMain.swift. You’ll see that ContentView uses .fullScreenCover. Now, to see how .sheet view modifier behaves, replace:

ContentView().fullScreenCover(
  isPresented: $shouldPresentOnboarding,
  onDismiss: nil
)

With:

ContentView().sheet(
  isPresented: $shouldPresentOnboarding,
  onDismiss: nil
)

Delete the app, so the onboarding screens show again. Build and run. You’ll see this view:

Onboarding screens using a sheet view modifier.
Onboarding screens using a sheet view modifier.

Both of these ways present the view in a bottom-to-top fashion.

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.

To create a user interface with TabView, you place views inside the TabView. Then you add .tabItem(_:) to the view contained inside the TabView, which helps toggle between the views.

To get a better understanding, open ContentView.swift and look at the following code:

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)
  }
}

This code creates two child views inside a TabView.

  1. The first tab item selection displays AnimalsNearYouView.
  2. The second tab item selection shows SearchView.

Note: .tabItem(_:) only supports a Text or an Image, a Text with an Image, or a Label. Adding another type of view results in empty tab item.

To assign a badge on your tab item, add .badge(2) before the Near you tabItem:

.badge(2)

Now, you’ll see 2 on the Near you tab.

Build and run. You’ll get the following result:

Near you tab with a badge of 2.
Near you tab with a badge of 2.

You can also use the selection initializer to perform navigation in TabView. The cool thing about selection is that it’s not limited to Int data type. You can pass in any object that conforms to Hashable.

Imagine you want to toggle between the child views within the TabView, AnimalsNearYouView and SearchView programmatically.

Start by creating PetSaveTabType.swift under Core/views. Then add:

enum PetSaveTabType {
  case nearYou
  case search
}

Since there are two child views in the TabView, you add two cases in the enum.

Under the same group, create PetSaveTabNavigator.swift and add:

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)
  }
}

Here’s an explanation to the code:

  1. The @Published property informs the UI as soon as its value changes.
  2. A method to set the different types of tabs.
  3. Since you’ll be using a custom type you need to conform to Hashable.

Now, back in ContentView.swift, create an object of PetSaveTabNavigator inside the ContentView:

@StateObject var tabNavigator = PetSaveTabNavigator()

The @StateObject creates the object and maintains its state. It refreshes the entire UI based on this object.

You’ll also update the TabView with its selection initializer and add tags to the views. Replace the body with:

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)
  }
}

Here’s a code breakdown:

  1. You pass in the currentTab from the PetSaveTabNavigator object to the selection initializer.
  2. Then, you apply a tag to AnimalsNearYouView.
  3. Finally, you apply a tag to SearchView.

Now you can change the tabs programmatically. How? call switchTab(_:) which resides inside PetSaveTabNavigator. Then you’ll see the tabs switch.

Note: You can achieve similar results by using the selection on NavigationView.

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.

Select the PetSave target and click info. Then unfold the URL Types.

Add URL Types.
Add URL Types.

Click the small +, add petsave to the URL Schemes and press return.

Add URL Schemes.
Add URL Schemes.

Now, open the PetSaveTabType.swift and add the following method to the enum:

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
}

This checks if the scheme is petsave. Then it checks if the host is either nearYou or search and returns the respective type. To keep things simple, the default type is .nearYou if the scheme doesn’t match petsave.

Then, open ContentView.swift and add the following modifier at the end of the TabView:

// 1
.onOpenURL { url in    
  // 2
  let type = PetSaveTabType.deepLinkType(url: url)
  // 3
  self.tabNavigator.switchTab(to: type)
}

Here, you:

  1. Receive the opened url.
  2. Get the correct tab type using url.
  3. Call switchTab(_:) to present the right tab depending on type.

Build and run. In the simulator, tap Home icon and open Safari browser. Type petsave://search in the browser and press return. You’ll see the following alert.

Deep link alert.
Deep link alert.

On the alert, tap Open, which takes you to the app’s search view.

PetSave's search opened using deep link.
PetSave's search opened using deep link.

Go back to the browser, type petsave://nearYou and tap go. When you see the alert, tap Open, and you’ll see:

PetSave's near you opened using deep link.
PetSave's near you opened using deep link.

Woooohoooo! The deep link works, and so does the programmatic switching of views. You did a great job!

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.

Moreover, go and check out our tutorial on SwiftUI navigation. You can also read the navigation tutorial from Apple. Also, you can read more in detail from Apple about the Types of Navigation.

In the next chapter, you’re going to add some more fun to the app. You’ll learn about adding animations and custom controls to the app’s UI while also abiding by Apple’s Human interface guidelines.

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.
© 2026 Kodeco Inc.