Routing
is a SwiftUI library that decouples navigation logic from views, promoting separation of concerns, improved maintainability, and flexible programmatic navigation.
- Enforces separation of concerns by de-coupling navigation logic from SwiftUI views.
- Supports flexible programmatic navigation.
- Type-safe
- Ideal for structured, hierarchical navigation
- Requirements
- Installation
- Getting Started
- Programmatic Navigation
- Deep Linking Support
- Additional Resources
- Usage With TabView
- Contributions
- Requires iOS 16 or later.
You can install Routing
using the Swift Package Manager.
- In Xcode, select "File" > "Add Packages...".
- Copy & paste the following into the "Search or Enter Package URL" search bar.
https://github.com/obvios/Routing.git
- Xcode will fetch the repository & the "Routing" library will be added to your project.
- Create a
Routable
conformingEnum
to represent the different views you wish to route to.
import SwiftUI
import Routing
enum ExampleRoute: Routable {
case root
case viewA
case viewB(String)
case viewC
@ViewBuilder
func viewToDisplay(router: Router<ExampleRoute>) -> some View {
switch self {
case .root:
RootView(router: router)
case .viewA:
ViewA(router: router)
case .viewB(let description):
ViewB(router: router, description: description)
case .viewC:
ViewC(router: router)
}
}
}
- Add the
RoutingView
to your view hierarchy and inject aRouter
instance.
import SwiftUI
import Routing
struct ContentView: View {
@StateObject var router: Router<ExampleRoute> = .init(isPresented: .constant(.none))
var body: some View {
RoutingView(router) { _ in
router.start(.root)
}
}
}
- Use the
Router
functions from any of your views. Here isViewA
.
struct ViewA: View {
@ObservedObject var router: Router<ExampleRoute>
var body: some View {
Text("View A")
Button("View A: Push") {
router.routeTo(.viewA, via: .push)
}
Button("View B: Full screen cover") {
router.routeTo(.viewB("Got here from View A"), via: .fullScreenCover)
}
Button("ViewC: Sheet") {
router.routeTo(.viewC, via: .sheet)
}
Button("Dismiss self") {
router.dismissSelf()
}
Button("Pop back") {
router.pop()
}
}
}
Using the Router
object you can easily control navigation programatically, here are some examples:
// Navigate to a destination using a specific navigation type
router.routeTo(.details(id: "123"), via: .push)
router.routeTo(.settings, via: .sheet)
router.routeTo(.profile, via: .fullScreenCover)
// Pop the last view from the navigation stack
router.pop()
// Pop to the root view
router.popToRoot()
// Replace the entire navigation stack
router.replaceNavigationStack(with: [.home, .profile])
// Dismiss the currently presented modal (sheet or full-screen cover)
router.dismissChild()
// Dismiss the entire RoutingView instance if it was presented
router.dismissSelf()
Routing
provides support for deep linking using the .onDeepLink(using:_:)
modifier.
This allows clients to handle incoming URLs and navigate to the appropriate Routable
destination.
NOTE: The library will dismiss any sheets/fullScreenCovers automatically if needed before displaying the deep linked view.
WARNING: I recommend only adding one .onDeepLink(using:_:)
modifier at the root of your view hierarchy.
Attach .onDeepLink(using:_:)
to RoutingView
to handle deep links:
import SwiftUI
import Routing
struct ContentView: View {
@StateObject var router: Router<ExampleRoute> = .init(isPresented: .constant(.none))
var body: some View {
RoutingView(router) { _ in
router.start(.root)
}
.onDeepLink(using: router) { url in
// Add your logic to handle deep link here
print(url)
// Return the destination to navigate to for the deep link
return .viewC
}
}
}
The below articles are from my blog series explaining the Router
pattern and documents the progress of this library.
- Learn about the Router pattern for SwiftUI navigation
- See how presentation was added
- Blog post explaining this Routing library
- Each tab should embed its own RoutingView instance to ensure navigation is managed independently within that tab. This allows each tab to maintain its own navigation stack while using a shared router for programmatic navigation.
To ensure proper navigation behavior in a multi-tab environment, structure your application like this:
import SwiftUI
import Routing
struct MainTabView: View {
@StateObject var routerA = Router<ExampleRouteA>(isPresented: .constant(nil))
@StateObject var routerB = Router<ExampleRouteB>(isPresented: .constant(nil))
var body: some View {
TabView {
RoutingView(routerA) { _ in
routerA.start(.rootA) // Sets the starting view for Tab 1
}
.tabItem {
Label("Tab 1", systemImage: "house")
}
RoutingView(routerB) { _ in
routerB.start(.rootB) // Sets the starting view for Tab 2
}
.tabItem {
Label("Tab 2", systemImage: "gear")
}
}
}
}
NOTE: As you may have noticed, currently the Router cannot be used to route to views contained in a different tab. This limitation is due to the original design of the library. This additional capability is in the works.
Pull requests and issues are always welcome! Please open any issues and PRs for bugs, features, documentation, or enhancements.