Releases: mtzaquia/departure
v1.0.0
Departure 1.0.0 is the initial release of a lightweight, expressive routing layer for SwiftUI.
Departure lets visible views declare the routes they own, then lets any active view request navigation through a single environment command. Route requests are resolved by ownership, not callback chains: Departure starts from the active route scope, crawls backward through visible scopes, and presents from the nearest eligible owner.
Highlights
SwiftUI-first routing
Install Departure once near the root of your app:
WithRouter {
NavigationStack {
HomeView()
}
}Then request routes from SwiftUI with:
@Environment(\.routing) private var routing
routing(.present(SettingsRoute()))Route declarations
Views declare the routes they own with .routes { ... }:
.routes {
Push(ProfileRoute.self)
Sheet(SettingsRoute.self)
Cover(OnboardingRoute.self)
}Supported presentation styles:
| Declaration | Behavior |
|---|---|
Push |
Uses navigationDestination(item:). |
Sheet |
Uses sheet(item:). |
Cover |
Uses full-screen cover on UIKit, sheet fallback elsewhere. |
Sheets and covers provide a NavigationStack by default, with providesNavigation: false available for destinations that provide their own container.
Branch-aware routing
Departure supports selection-based containers such as tabs:
.routes(branch: $tab) {
Branch(.home) {
Push(HomeDetailRoute.self)
}
Branch(.wallet) {
Sheet(TransactionRoute.self)
}
}Branch declarations are discoverable even when SwiftUI has not mounted a lazy branch yet. Matching .routeBranch(value) views adopt those declarations as local presentation hosts once mounted.
If a route belongs to an inactive branch, Departure selects that branch first, waits for the branch host, then presents from the correct local scope.
High-priority presentations
Sheets and covers can be marked .high:
.routes {
Cover(LoginRoute.self, priority: .high)
}High-priority routes present above the normal app flow in a separate UIKit window. Normal route requests are blocked while a high-priority flow is active, unless they are local to that high-priority flow.
Route resolution
Routes can redirect or cancel themselves before presentation:
func resolveRoute() async -> (any Route)? {
isLoggedIn ? self : LoginRoute()
}Return self to continue, another route to redirect, or nil to drop the request.
Actions
Departure includes route-aware actions:
routing(.perform(SaveDraftAction()))Actions run against the active route context. If an action throws .reroute(route), Departure presents that route and retries the action once from the new active scope.
Hooks and interception
Views can attach route-scoped hooks:
.hooks {
ActionInterceptor(SaveDraftAction.self) { invocation in
try await invocation()
}
}Hooks disappear with their owning route scope. Inside selected branches, hooks attach to the branch-local scope, so tab-specific views can intercept actions only while selected.
Unwind
Departure provides dismissal and continuation routing:
routing(.unwind())
routing(.unwind(to: .root))
routing(.unwind(to: .id("settings-flow")))
routing(.unwind(to: .root, thenPresent: LoginRoute()))Unwind removes dismissed route scopes first, waits for SwiftUI to dismantle mounted views, then runs any continuation route through the normal routing rules.