Cheatsheet for SwiftUI Navigation

Cheatsheet for SwiftUI Navigation
Photo by Glenn Carstens-Peters / Unsplash

How navigation works in SwiftUI often proves to be confusing for new folks. In spite of the apparent attempt by Apple engineers to make app development easier for developers, navigation is one area that is confusing, counterintuitive, and difficult to handle in SwiftUI.

In this blog post, I am going to list 3 common ways for navigating around SwiftUI screens with a cheat sheet to quickly inject the navigation code into your app.

  1. Navigation with a navigation view (Push or pop screen)
  2. Presenting a partial modal screen (Partial)
  3. Presenting a full modal screen (Full)

This cheat sheet will be useful for common navigation references and any time you want to write code for specific navigation.

tl;dr;

Even if you don't have enough time to go through the full article, here's the quick cheat sheet for SwiftUI navigation APIs.

The code is provided with a placeholder text so that you can simply copy and paste these snippets into your code replacing placeholders,

Push Based Navigation


NavigationView {
    NavigationLink(isActive: <#T##Binding<Bool>#>,
                   destination: <#T##() -> _#>,
                   label: <#T##() -> _#>
    )
}

Partial Modal Presentation


<Any View Type>.sheet(
    isPresented: <#T##Binding<Bool>#>,
    onDismiss: <#T##(() -> Void)?##(() -> Void)?##() -> Void#>,
    content: <#T##() -> View#>
)

Full-screen Modal Presentation


<Any View Type>.fullScreenCover(
    isPresented: <#T##Binding<Bool>#>,
    onDismiss: <#T##(() -> Void)?##(() -> Void)?##() -> Void#>,
    content: <#T##() -> View#>
)

Now that we're past the cheat sheet, let's take a look at each way one-by-one

Push Based Navigation with a Navigation View

This kind of navigation is similar to UINavigationController-based navigation in the UIKit system. In order to trigger navigation with NavigationView which involves a push-based transition to the new screen, you can use the NavigationLink API with the following parameters.


NavigationView {
	NavigationLink(
    	isActive: <is_active>, 
        destination: <destination>, 
        label: <label>
	)
}
💡
Please note that, in order to make push-based navigation work, you must wrap the root view element in NavigationView component, which is essentially a navigation controller of SwiftUI system

Please note how NavigationLink is wrapped into the NavigationView. Wrapping it into NavigationView makes it part of the navigation stack and any time navigation link is activated, it pushes the destination screen on the navigation stack.

Now that the API is here, let's take a deep look at its parameters,

  1. isActive  - Takes the binding variable as input. The link is active/inactive based on the true or false value of this variable
  2. destination - Refers to a view for the navigation link to present once the link is activated through isActive parameter
  3. label - A view builder to produce a label describing the destination to present once the link is activated. For example, you can have a button that activates the link when the user taps on it.

Let's take a look at an example and working demo of NavigationView-based navigation in SwiftUI,

Here's a sample code for destination TestScreen,


struct TestScreen: View {
    var body: some View {
        Text("Welcome to Test Screen")
    }
}

And here's a code for NavigationLink to navigate to TestScreen,


struct LandingScreen: View {

	@State private var showTestScreen = false
    
    var body: some View {
    NavigationView {
    	//...Some other views
        NavigationLink(isActive: $showTestScreen) {
            TestScreen()
        } label: {
            Button("Show Test Screen") {
                showTestScreen = true
            }
        }
        //...Some more other views
    }
}
0:00
/

Partial Modal Presentation

Another way to perform screen navigation is to use public func sheet<Content>(isPresented.... API to present a new screen in SwiftUI. It presents a new screen in such a way that it leaves slight gap on the top and can be dismissed by vertically dragging the screen from top to bottom.

In order to present a new screen, you can use sheet API, which is added in the form of an extension on the View type.


public func sheet<Content>(isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping () -> Content) -> some View where Content : View
  1. isPresented  - Takes the binding variable as an input. Used to control whether the sheet is presented or not
  2. onDismiss - A closure that gets called every time the screen is dismissed
  3. content: Represents a screen to present once binding variables passed to isPresented argument is true

Now that we have a theoretical understanding of API to present a modal screen, let's write a code to see it in action.


@State private var showTestScreen = false

var body: some View {
    Button("Show Test Screen") {
        showTestScreen = true
    }.sheet(isPresented: $showTestScreen) {
        // Things to do when the screen is dismissed
    } content: {
    	// Destination screen
        TestScreen()
    }
}
0:00
/

Full Modal Presentation

There is one more way to navigate to new screen by presenting it in full-screen format. The fullScreenCover API defined on View type can be used here. This API has the following signature,


public func fullScreenCover<Content>(isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping () -> Content) -> some View
  1. isPresented - Takes the binding variable to control whether the full-screen sheet is presented or not
  2. onDismiss - A closure that gets called every time the screen is dismissed
  3. content - Represents a screen to present once the binding variable passed to  the isPresented argument is true

Let's take a look at code to present the full screen sheet and related demo,


@State private var showTestScreen = false

var body: some View {
    Button("Show Test Screen") {
        showTestScreen = true
    }.fullScreenCover(isPresented: $showTestScreen) {
        // Things to do when the screen is dismissed
    } content: {
        TestScreen()
    }
}
0:00
/

Unlike the previous example involving a partial modal screen which can be dismissed by a vertical drag gesture, there is no automatic way to dismiss the full-screen presented screen.

To fix this, we will pass the showTestScreen in landing screen as a binding variable to TestScreen. TestScreen will take this binding variable as input, and set its value to false when the user taps on the "Close" button on this screen.

Once showTestScreen is set to false, isPresented passed to fullScreenCover API on the previous screen will cease to be true and full-screen cover will be dismissed.


//TestScreen.swift

struct TestScreen: View {
    
    @Binding var showTestScreen: Bool
    
    var body: some View {
        Text("Welcome to Test Screen")
        
        Button("Close") {
            showTestScreen = false
        }
    }
}

//LandingScreen.swift

struct LandingScreen: View {

	@State private var showTestScreen = false

    var body: some View {
        Button("Show Test Screen") {
            showTestScreen = true
        }.fullScreenCover(isPresented: $showTestScreen) {
            //no-op
        } content: {
            TestScreen(showTestScreen: $showTestScreen)
        }
    }
}

Now that we set up state/binding variables dance, we can easily dismiss the presented full-screen sheet just by toggling the value of this variable from the TestScreen

0:00
/

Summary

So this was all about handling common navigations in SwiftUI using three basic navigation techniques. I always struggle to find the correct API and syntax for handling these scenarios, so I figured it would be far easier to document them in one place with coding examples and a demo.

Hope you liked this article and thanks for reading it. If you have any comments, questions, or feedback, please reach out to me on Twitter @jayeshkawli.