Skip to content

Navigation Router

Alex Nagy edited this page Mar 2, 2023 · 12 revisions

Stop wrestling SwiftUI Navigation

With this library you can finally have a SwiftUI Navigation as EASY as it should have been in the first place.

Features

Here's a list of features Navigation Router has to offer:

  • Simple and unified API
  • Supports iOS, macOS, tvOS and watchOS
  • Has async/await API
  • Has completion handler API
  • Supports iOS16
  • Supports SwiftUI4
  • Navigate to any view from any view
  • Supports Dependency Injection (move data between views easily)
  • Perfect for deep links
  • Setup all your destinations in an enum
  • Navigate with ONLY 1 line of code
  • Supports stack, .sheet and .fullScreenCover navigation
  • Create a flow for ANY type of navigation
  • Built on top of NavigationStack introduced in SwiftUI4
  • Type-safe navigation
  • Pop to root
  • Pop any number of views
  • Pop to an index
  • Pop to nearest root
  • Pop one by one
  • Built in Swift 5.7 and SwiftUI 4
  • Example Xcode Project
  • 100% Documented

Getting started

First you want to do some setup so you can use the Navigation API effortlessly later on.

Create destination Views

First we want to create Views that we are going to navigate to. Of course any of these Views can be used as a root.

import SwiftUI
import kindaSwiftUI

struct CroissantView: View {
    var body: some View {
        List {
            
        }
        .navigationTitle("πŸ₯")
    }
}
import SwiftUI
import kindaSwiftUI

struct PizzaView: View {
    var body: some View {
        List {
            
        }
        .navigationTitle("πŸ•")
    }
}
import SwiftUI
import kindaSwiftUI

struct FruitView: View {

    let title: String

    var body: some View {
        List {
            
        }
        .navigationTitle(title)
    }
}

Note that we added the title as a String dependency to the FruitView.

Create a NavigationRouterSetup file

With NavigationRouter you create your Destinations as enums and then navigate to them in a type-safe manner. Create an enum that adheres to the RouterDestination protocol.

enum Destination: RouterDestination {
    
}

Next add in a defaultView and all your other view destinations as cases. We need the defaultView later on for modal navigation setup.

enum Destination: RouterDestination {
    case defaultView
    
    case croissant
    case pizza
    case fruit(dependency: String)
}

Note that any destination can have only one dependency of any type. In our example the fruit has a String dependency.

Next add the var modalValue: ModalValue protocol requirement. We give the defaultView a ModalValue index of -1 and afterwards we just use a unique Int. Feel free to just simply count up from 0. In order to handle dependency injection we use the ModalValue(index:, dependency:) initialiser.

    var modalValue: ModalValue {
        switch self {
        case .defaultView: return ModalValue(index: -1)
        case .croissant: return ModalValue(index: 0)
        case .pizza: return ModalValue(index: 1)
        case .fruit(let dependency): return ModalValue(index: 2, dependency: dependency)
        }
    }

Next we can create the init?(modalValue: ModalValue). Add this below your modalValue property.

    init?(modalValue: ModalValue) {
        switch modalValue.index {
        case 0: self = .croissant
        case 1: self = .pizza
        case 2: self = .fruit(dependency: modalValue.dependency as? String ?? "")
        default: self = .defaultView
        }
    }

Notes:

  • we need to unwrap the type of the dependency
  • for the default case we use defaultView

Finally we add in our Views for the cases created. We use an EmptyView() for the defaultView. We also pass along the dependency to the View.

    var body: some View {
        switch self {
        case .defaultView:
            EmptyView()
            
        case .croissant:
            CroissantView()
        case .pizza:
            PizzaView()
        case .fruit(let title):
            FruitView(title: title)
        }
    }

This is how your NavigationRouterSetup file should look like right now:

import SwiftUI
import kindaSwiftUI

enum Destination: RouterDestination {
    case defaultView
    
    case croissant
    case pizza
    case fruit(dependency: String)
    
    var modalValue: ModalValue {
        switch self {
        case .defaultView: return ModalValue(index: -1)
        case .croissant: return ModalValue(index: 0)
        case .pizza: return ModalValue(index: 1)
        case .fruit(let dependency): return ModalValue(index: 2, dependency: dependency)
        }
    }
    
    init?(modalValue: ModalValue) {
        switch modalValue.index {
        case 0: self = .croissant
        case 1: self = .pizza
        case 2: self = .fruit(dependency: modalValue.dependency as? String ?? "")
        default: self = .defaultView
        }
    }
    
    var body: some View {
        switch self {
        case .defaultView:
            EmptyView()
            
        case .croissant:
            CroissantView()
        case .pizza:
            PizzaView()
        case .fruit(let title):
            FruitView(title: title)
        }
    }
}

Create a RouterStack for your <Destination> with a root that you choose

The easiest way to spin off a Navigation Router is to create a RouterStack(root:).

RouterStack<Destination>(root: .croissant)

Your ContentView will now look like this:

import SwiftUI
import kindaSwiftUI

struct ContentView: View {
    var body: some View {
        RouterStack<Destination>(root: .croissant)
    }
}

How to access the Router

From now on you can access the Router as an @EnvironmentObject in any of your Views.

@EnvironmentObject private var router: Router<Destination>

Note that we provide the Destination that we created earlier for the RouterDestination of the Router.

Navigate all the things

Now it's time to learn about the amazing NavigationRouter API.

Push a View

We are talking about pushing a View when we navigate from one View to another in a stack like manner.

To push the PizzaView from the CroissantView all you have to do is use the push(_:) function on the router.

router.push(.pizza)

Your CroissantView will look like this:

struct CroissantView: View {
    
    @EnvironmentObject private var router: Router<Destination>
    
    var body: some View {
        List {
            Button("Push πŸ•") {
                router.push(.pizza)
            }
        }
        .navigationTitle("πŸ₯")
    }
}