Skip to content

Releases: mtzaquia/departure

v1.0.0

08 Jun 16:46

Choose a tag to compare

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.