Skip to content

gabriel/swiftui-routes

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

46 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🧭 SwiftUI Routes

SwiftUI Routes is a simple library that is minimal and flexible to register routes and navigate to them.

✨ Features

  • 🎯 Flexible Navigation: Works seamlessly with custom navigation frameworks or presenters, or Apple's NavigationStack and sheets
  • 🎨 Simple & Declarative: Centralized route registration with minimal boilerplate
  • πŸ”’ Type-Safe Routing: Navigate using strings ("/album/123") or strongly-typed values (Album(id: "123"))
  • πŸ“¦ Multi-Package Support: Register and share a single Routes instance across SPM packages
  • πŸ”— Deep Linking Ready: Built-in URL parsing and route matching for deep link handling
  • πŸ“ˆ Scalable Architecture: Clean separation of concerns that grows with your project

πŸ“ Register

Start by creating a Routes.swift file and registering destinations. Registrations accept either a resource path (string) or a Routable value. Paths can be parameterized to include params (like id).

import SwiftUIRoutes

@MainActor
var routes: Routes {
    let routes = Routes()
    routes.register(path: "/album/:id") { route in
        if let id = route.param("id") {
            AlbumView(id: id)
        }
    }

    routes.register(type: Album.self) { album in
        AlbumDetailView(album: album)
    }
    return routes
}
  • πŸ›€οΈ Path registrations use URL-style patterns. The closure receives a Route so you can pull out parameters or query items with route.param(_:) or route.params.
  • 🏷️ Type registrations work with any Routable. Conforming types define how to turn a value into the resource path that should be presented.
struct Album: Routable {
    var id: String

    var route: Route { Route("/album/\(id)") }
}

🎡 Example Project

Explore Examples/MusicApp for a complete sample integrating SwiftUI Routes; open Examples/MusicApp/MusicApp.xcodeproj in Xcode to run it.

πŸ“± Requirements

  • iOS 17.0+ / macOS 15.0+
  • Swift 6.0+

πŸ“¦ Installation

Swift Package Manager

dependencies: [
    .package(url: "https://github.com/gabriel/swiftui-routes", from: "0.2.1")
]

.target(
    dependencies: [
        .product(name: "SwiftUIRoutes", package: "swiftui-routes"),
    ]
)

πŸš€ NavigationStack

Attach your routes to a NavigationStack by keeping a RoutePath binding. The modifier installs every registered destination and exposes the binding through EnvironmentValues.routePath. Define routesDestination on the root view.

struct AppScene: View {
    @State private var path = RoutePath()

    var body: some View {
        NavigationStack(path: $path) {
            HomeView()
                .routesDestination(routes: routes, path: $path)
        }
    }    
}

Views can access the routePath from the environment (view hierarchy) and can push routes directly or use the provided view modifiers.

struct HomeView: View {
    @Environment(\.routePath) private var path

    var body: some View {
        VStack {
            Button("Album (123)") {
                path.push("/album/123")
            }

            Button("Featured Album") {
                path.push(Album(id: "featured"))
            }

            Text("Tap to open Latest")
                .push(Album(id: "123"), style: .tap)
        }
    }
}

The push(_:style:) modifier wraps any view in a navigation trigger while still using the same registrations.

πŸ”— Deep Linking

Handle deep links by converting incoming URLs to routes and pushing them onto the navigation path. Use onOpenURL(perform:) and create a Route from the URL:

struct AppScene: App {
    @State private var path = RoutePath()
    @State private var sheet: Routable?

    var body: some Scene {
        WindowGroup {
            NavigationStack(path: $path) {
                HomeView()
                    .routesDestination(routes: routes, path: $path)
            }
            .routeSheet(routes: routes, item: $sheet)
            .onOpenURL(perform: handleDeepLink(_:))
        }
    }

    private func handleDeepLink(_ url: URL) {
        let route = Route(url: url)

        // Check for sheet presentation parameter
        if route.param("presentation") == "sheet" {
            sheet = route
            return
        }

        // Push onto the navigation stack
        sheet = nil
        path.push(route)
    }
}

The Route(url:) initializer extracts the path and query parameters from the URL, matching them against your registered patterns:

  • 🎡 myapp://album/123 β†’ matches /album/:id with id=123
  • πŸ“„ myapp://album/123?presentation=sheet β†’ same route but presented as a sheet via the presentation parameter
  • 🌐 myapp://album/featured?lang=en β†’ matches /album/:id with id=featured and query param lang=en

πŸ‘οΈ View from a Route

Use Routes.view(_:) to render a destination directly from a registered path or type, if you don't want to use NavigationStack or have a custom setup.

struct MyRouteViews: View {
    var body: some View {
        VStack(spacing: 16) {
            routes.view("/album/123")
            routes.view(Album(id: "featured"))
        }
    }
}

πŸ“„ Sheets

Define a sheet binding and use routeSheet. If stacked is true, it will wrap the route view in another NavigationStack in case those views also push or pop.

struct HomeView: View {
    @State private var sheet: Routable?
    
    let album: Album

    var body: some View {
        VStack {
            Button("Open Album") {
                sheet = album
            }
        }
        // If stacked is true will wrap in a new NavigationStack configured with these routes
        .routeSheet(routes: routes, item: $sheet, stacked: true) 
    }
}

πŸ“¦ Multiple Packages

Share a single Routes instance across packages without creating cyclical dependencies by letting each package contribute its own registrations. The app owns the Routes instance and passes it to package-level helpers that fill in the routes it knows about. Create a public func register(routes: Routes) method in each package.

import PackageA
import PackageB
import SwiftUI
import SwiftUIRoutes

@MainActor
var routes: Routes {
    let routes = Routes()
    PackageA.register(routes: routes)
    PackageB.register(routes: routes)
    return routes
}

In PackageC:

import SwiftUI
import SwiftUIRoutes

public struct ExampleView: View {
    let routes: Routes
    @State private var path = RoutePath()

    public var body: some View {
        NavigationStack(path: $path) {
            List {
                Button("A view in PackageA") {
                    path.push("/a/1", params: ["text": "Hello!"])
                }

                Button("A view in PackageB") {
                    path.push("/b/2", params: ["systemName": "heart"])
                }
            }
            .routesDestination(routes: routes, path: $path)
        }
    }
}

Each package exposes a simple register(routes:) entry point so it never needs to import another package’s views.

In PackageA:

import SwiftUI
import SwiftUIRoutes

public func register(routes: Routes) {
    routes.register(path: "/a/:id") { url in
        Text(url.params["text"])
    }
}

In PackageB:

import SwiftUI
import SwiftUIRoutes

public func register(routes: Routes) {
    routes.register(path: "/b/:id") { url in
        Image(systemName: url.params["systemName"] ?? "heart.fill")
    }
}

This keeps navigation declarative and avoids mutual dependencies between packages because the shared Routes instance lives in the root target while features register themselves.

About

SwiftUI routing from URLs or Types

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages