The navigation framework for SwiftUI.
sRouting provides a native navigation mechanism that simplifies handling navigation between screens.
- iOS 17 or above
- Xcode 16 or above
Explore DocC to find rich tutorials and get started with sRouting. See this WWDC presentation about more information.
From Xcode select Product -> Build Documentation -> Explore.
Add sRouting as a dependency to the project.
See this WWDC presentation for more information on how to adopt Swift packages in your app.
Specify https://github.com/ThangKM/sRouting.git as the sRouting package link.
Explore the example branch: Example
Set up the SRRootView and interact with macros.
Create your root view using SRRootView.
Declare your SRRoute.
Learn about macros and ViewModifiers.
To create a route, we must adhere to the SRRoute Protocol.
@sRoute
enum HomeRoute {
typealias AlertRoute = YourAlertRoute // Optional declarations
typealias ConfirmationDialogRoute = YourConfirmationDialogRoute // Optional declarations
typealias PopoverRoute = YourPopoverRoute // Optional declarations
case pastry
case cake
@sSubRoute
case detail(DetailRoute)
@ViewBuilder @MainActor
var screen: some View {
switch self {
case .pastry: PastryScreen()
case .cake: CakeScreen()
case .detail(let route): route.screen
}
}
}If you need to define paths for SRRoute manually, for example, to use with a specific actor (Swift Approachable concurrency), you can use the @sRoutePath macro.
This macro generates the Paths enum and path property but requires you to manually conform to the SRRoute protocol.
@sRoutePath
enum HomeRoute: SRRoute {
typealias AlertRoute = YourAlertRoute // Optional declarations
typealias ConfirmationDialogRoute = YourConfirmationDialogRoute // Optional declarations
typealias PopoverRoute = YourPopoverRoute // Optional declarations
case pastry
case cake
@sSubRoute
case detail(DetailRoute)
@ViewBuilder @MainActor
var screen: some View {
switch self {
case .pastry: PastryScreen()
case .cake: CakeScreen()
case .detail(let route): route.screen
}
}
}Start by configuring a coordinator and SRRootView for your application.
Declaring a Coordinator:
@sRouteCoordinator(tabs: ["home", "setting"], stacks: "home", "setting")
@Observable
final class AppCoordinator { }Declaring the View for Navigation Destinations:
@sRouteObserver(HomeRoute.self, SettingRoute.self)
struct RouteObserver { }Configuring Your App:
@sRoute
enum AppRoute {
case startScreen
case mainTabbar
@ViewBuilder @MainActor
var screen: some View {
switch self {
case .startScreen:
StartScreen()
.transition(.scale(scale: 0.1).combined(with: .opacity))
case .mainTabbar:
MainScreen()
.transition(.opacity)
}
}
}
struct MainScreen: View {
@Environment(AppCoordinator.self) var coordinator
var body: some View {
@Bindable var emitter = coordinator.emitter
TabView(selection: $emitter.tabSelection) {
NavigationStack(path: coordinator.homePath) {
HomeScreen()
.routeObserver(RouteObserver.self)
}
.tag(AppCoordinator.SRTabItem.homeItem.rawValue)
NavigationStack(path: coordinator.settingPath) {
SettingScreen()
.routeObserver(RouteObserver.self)
}
.tag(AppCoordinator.SRTabItem.settingItem.rawValue)
}
}
}
@main
struct BookieApp: App {
@State private var appCoordinator = AppCoordinator()
@State private var context = SRContext()
var body: some Scene {
WindowGroup {
SRRootView(context: context, coordinator: appCoordinator) {
SRSwitchView(startingWith: AppRoute.startScreen)
}
.environment(appCoordinator)
}
}
}Use the onRouting(of:) view modifier to observe route transitions.
@sRoute
enum HomeRoute {
case detail
...
}
struct HomeScreen: View {
@State private var homeRouter = SRRouter(HomeRoute.self)
var body: some View {
VStack {
...
}
.onRouting(of: homeRouter)
}
DeepLink:
...
.onOpenURL { url in
Task {
...
await context.routing(.resetAll,
.select(tabItem: .home),
.push(route: HomeRoute.cake))
}
}To observe and open a new coordinator from the router, use onRoutingCoordinator(_:context:).
Declaring Coordinator Routes:
@sRouteCoordinator(stacks: "newStack")
final class AnyCoordinator { }
struct AnyCoordinatorView<Content>: View where Content: View {
@Environment(SRContext.self) var context
@State private var coordinator: AnyCoordinator = .init()
let content: () -> Content
var body: some View {
SRRootView(context: context, coordinator: coordinator) {
NavigationStack(path: coordinator.newStackPath) {
content()
.routeObserver(YourRouteObserver.self)
}
}
}
}
@sRoute
enum CoordinatorsRoute {
case notifications
case settings
@MainActor @ViewBuilder
var screen: some View {
switch self {
case .notifications:
AnyCoordinatorView { NotificationsScreen() }
case .settings:
AnyCoordinatorView { SettingsScreen() }
}
}
}Handling Coordinator Routing in the Root View:
Coordinators should be triggered from the root view using .onRoutingCoordinator.
@main
struct BookieApp: App {
@State private var appCoordinator = AppCoordinator()
@State private var context = SRContext()
var body: some Scene {
WindowGroup {
SRRootView(context: context, coordinator: appCoordinator) {
SRSwitchView(startingWith: AppRoute.startScreen)
}
.environment(appCoordinator)
.onRoutingCoordinator(CoordinatorsRoute.self, context: context)
}
}
}Present a new coordinator:
router.openCoordinator(route: CoordinatorsRoute.notifications, with: .present)Change Root:
router.switchTo(route: AppRoute.mainTabbar)Push:
router.trigger(to: .cake, with: .push)NavigationLink:
NavigationLink(route: HomeRoute.pastry) {
...
}Present full screen:
router.trigger(to: .cake, with: .present)Sheet:
router.trigger(to: .cake, with: .sheet)To show an alert we use the show(alert:) function.
router.show(alert: YourAlertRoute.alert)To dismiss a screen we use the dismiss() function.
router.dismiss()To dismiss to root view we use the dismissAll() function.
Required the root view is a SRRootView
router.dismissAll()To select the Tabbar item we use the selectTabbar(at:) function.
router.selectTabbar(at: AppCoordinator.SRTabItem.home)Pop Actions in NavigationStack
router.pop()
router.popToRoot()
router.pop(to: HomeRoute.Paths.cake)sRouting is released under an MIT license. See License.md for more information.

