Skip to main content
  1. Blog/

A SwiftUI Router

app programming Swift Talk Dim Sum
Phil Chu
Author
Phil Chu
Making software since the 80s

One update I made to Talk Dim Sum this year was to add a custom router so I can escape the NavigationLink straightjacket.

My router is just a simple stack.

@Observable
public class Router<T: Equatable> {

    public var path: [T] = []

    public init() {
    }

    public func push(_ route: T) {
        path.append(route)
    }

    public func pushNew(_ route: T) {
        if path.last != route {
            push(route)
        }
    }

    public func pop() {
        if !path.isEmpty {
            path.removeLast()
        }
    }

    func clear() {
        path = []
    }
}

It’s generic so I can use it in more than one app, each with a different set of routes. Here’s the one for my dim sum app, just listing the routes for bringing up a camera view and for displaying the image taken.

enum Route: Hashable {
    case mlCamera
    case mlImage(UIImage)
}

The enum cases have to be mapped to actual views.

struct DestinationView: View {

    let route: Route

    var body: some View {
        switch route {
        case .mlCamera:
            MLCameraView()
        case let .mlImage(image):
            MLImageView(image: image)
        }
    }
}

Now I can instantiate the router at the top level of my app and pass it down as an environment object.

@State var router = Router<Route>()

    var body: some Scene {
        WindowGroup {
            AppView()
                .environment(router)
        }
    }

First supplying it to the NavigationStack, and within that establish the navigation destination.

 @Environment(Router<Route>.self) var router: Router<Route>

    var body: some View {
        @Bindable var router = router
        NavigationStack(path: $router.path) {
            Tabs()
            .navigationDestination(for: Route.self) { route in
                DestinationView(route: route)
            }
        }
    }

Now it’s ready to use, e.g. this line pushes a new view on the stack (if it’s not already there)

router.pushNew(.mlImage(image))