arrow left
Back to Developer Education

Getting Started with Pragmatic Navigation in SwiftUI

Getting Started with Pragmatic Navigation in SwiftUI

SwiftUI is a new UI library used to build user interfaces in Swift. This declarative framework allows developers to build your UI declaratively. <!--more--> SwiftUI is data-driven, as opposed to the UIKIt framework that is imperative.

In this article, we will look at different methods of navigation flow and view construction in SwiftUI.

We will also look at the MVC pattern that will allow us to abstract the navigation logic from the view.

Table of contents

Prerequisites

To follow along with this article, you need:

MVC pattern

Model View Controller MVC is a design pattern that allows us to modularize our application and abstract the business logic from the view.

We will implement the MVC pattern in our application to abstract the navigation logic from the view and achieve the pragmatic SwiftUI navigation.

Application setup

On Xcode, create a new application named SwiftUI_Navigation. Make sure you have selected SwiftUI as the project type.

In the project directory, create a new group named Views and a new SwiftUI view file named HomeView.swift in the views group.

Update the HomeView.swift file with the following code:

struct HomeView: View {
    @State private var activeIndex = 0

    var body: some View {
        TabView(selection: $activeIndex) {
            Button("Tab B") {
                activeIndex = 1
            }
                    .tag(0)
                    .tabItem {
                        Label("A", systemImage: "a.circle")
                    }

            Button("Tab A") {
                activeIndex = 0
            }
                    .tag(1)
                    .tabItem {
                        Label("B", systemImage: "b.circle")
                    }
        }
    }
}

@State is a SwiftUI property that allows you to store a value in a view. It is not possible to hold a variable whose value changes in a SwiftUI view by default.

To overcome this problem, we use the @State property wrapper.

The above code creates a TabView that allows you to switch between two tabs. However, this kind of navigation leads to a lot of boilerplate code in the SwiftUI view.

This is evident when the number of navigation destinations increases. Let's say having ten screens to navigate.

To solve this problem, we can abstract the routes to a controller that will handle navigation.

Controller

In the project directory, create a new group named Controllers and a new Swift file name Navigation.swift and add the code snippet below:

enum Tab {
    case Tab_a
    case Tab_b
    case Tab_c
}

class HomeController: ObservableObject {
    @Published var active = Tab.Tab_a

    func navigate(tab: Tab) {
        active = tab //sets the current active tab from the View file
    }
}

In the code snippet above:

  • The enum Tab will store all the navigation destinations' names. Storing the destinations as enums makes it possible to keep the tabs' current state and navigate from any screen.

  • The HomeController class extends the ObservableObject since we have to keep track of the currently active tab on the SwiftUI view.

  • @Published annotation sets the variable active to be observable, making it possible for our SwiftUI views to observe the change and navigate to the select screen.

Model

In the project directory, create a new group named Models and create a new Swift file named News.swift and add the following code:

struct News: Identifiable {
    let id = UUID() //Autogenerated Value
    let title: String
    let description:String
}

In the above code, we have the news model that holds the news item. The News class extends Identifiable to make it possible to have a unique id that we can use to identify a news item.

View

Create a new Swift file named NewsView.swift in the Views group and add the code snippet below:

struct NewsView: View {
    var news: News //Single news item
    var body: some View {
        VStack {
            Text(news.title)
                    .font(.headline)
            Text(news.description)
                    .font(.body)
        }
    }
}

In the code snippet above, we create a view that will hold a single news item on the home screen.

We need to create a SwiftUI file named TabAView.Swift in the Views group and add the code below:

struct TabAView: View {
    @EnvironmentObject private var controller: HomeController
    //We create a dummy list of news items. In a real application, this would come from an API
    let news = [
        News(title: "News A", description: "DescriptionA"),
        News(title: "News A", description: "DescriptionA"),
        News(title: "News A", description: "DescriptionA"),
        News(title: "News A", description: "DescriptionA")
    ]

    var body: some View {
        NavigationView {
            VStack {
                Text("Home")
                        .font(.body)
                Spacer()
                Button("Search Items") {
                    controller.navigate(tab: .Tab_b)//navigating to the next view
                }
                //List of news Items
                List(news) { newsItems in
                    NewsView(news: newsItems)
                }
            }

        }
    }
}
  • @EnvironmentObject annotation is a Swift wrapper used on variables accessed by several views in the application. Whenever the observable variable in the HomeController changes, the view files should be able to update the value of the @EnvironmentObject variable.

We can also navigate to the next view through the NavigationLink provided by SwiftUI and a button click.

Next, create a new SwiftUI file named TabBView.swift in the Views group and add the following code:

struct TabBView: View {
    @EnvironmentObject private var controller: HomeController
    @State private var search = ""

    var body: some View {
        NavigationView {
            Text("Searching \(search)")
                    .searchable(text: $search)
                    .navigationTitle("Search")
        }
    }
}
  • In the above code, we created a SwiftUI view with a search field that can be used to search the news items.

  • @State annotation is a Swift wrapper that keeps the current state of variables whose values change. For example, whenever we input a character to the search text field, we store the current search text in the search variable.

Finally, we need to update the HomeView.swift with the code snippet below:

struct HomeView: View {
    @StateObject private var homeController = HomeController()

    var body: some View {
        TabView(selection: $homeController.active) {
            TabAView()
                    .tag(Tab.Tab_a)
                    .tabItem {
                        Label("Tab A", systemImage: "house")
                    }

            TabBView()
                    .tag(Tab.Tab_b)
                    .tabItem {
                        Label("Tab B", systemImage: "magnifyingglass")
                    }

            TabCView()
                    .tag(Tab.Tab_c)
                    .tabItem {
                        Label("Tab C", systemImage: "gearshape")
                    }
        }
                .environmentObject(homeController)
    }
}

In the above code, we are setting up the application tabs to enable us to navigate from one tab to another using SwiftUI tabs.

Conclusion

In this article, you have learned about the different techniques of navigation flow and view construction in SwiftUI.

We also discussed the MVC pattern that allows us to abstract the navigation logic from the view.

In addition, the MVC pattern makes it easier to modularize the application, thus making it easier to maintain it.

You can now complete the application's search functionality and implement the detail page.

You can download the complete source code here.


Peer Review Contributions by: Odhiambo Paul

Published on: Mar 2, 2022
Updated on: Jul 15, 2024
CTA

Start your journey with Cloudzilla

With Cloudzilla, apps freely roam across a global cloud with unbeatable simplicity and cost efficiency