Provides deep link routing in SwiftUI applications powered by SwiftDux.
SwiftDux Navigation implements deep-link routing for SwiftUI applications. It's currently in an early development stage.
The purpose of this library is to provide a stateful, deep-link navigational system for an application. In the same way that SwiftUI views represent the application's current state, it does the same for navigation. It's built to be used on its own or in an existing SwiftDux application.
Checkout the SwiftDux Todo Example.
- Add a SceneRoute to the root of a scene. It will use its own SwiftDux store internally.
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
SceneRoute {
ContentView()
}
}
}
}
-
Add navigation support to the application state by adhering to the
NavigationStateRoot
protocol.struct AppState: NavigationStateRoot { var navigation: NavigationState = NavigationState() }
-
Add the
NavigationReducer
andNavigationMiddleware
to the store.Store( state: AppState(), reducer: AppReducer() + NavigationReducer(), middleware: NavigationMiddleware() + PersistStateMiddleware(JSONStatePersistor()) )
-
Add a SceneRoute to the root of the application, making sure the store is provided to it.
@main struct MyApp: App { var store = Store( state: AppState(), reducer: AppReducer() + NavigationReducer(), middleware: NavigationMiddleware()) var body: some Scene { WindowGroup { SceneRoute(store: store) { ContentView() } } } }
The library uses specific terminology for the different parts of navigation. Below shows the navigational structure of a notes app. It's broken up into three types of components:
- Routes - Navigational paths within the application. The notes app has 4 possible routes:
- "/"
- "/settings"
- "/notes"
- "/notes/{id}"
- Waypoints - Individual destinations within a route. A route is made up of 2 or more waypoints. The last waypoint becomes the active destination. Each screen in the notes app represents a single waypoint.
- Legs - Segments that connect one waypoint to another within a route.
Similar to a NavigationLink, it uses a relative path to navigate to a different waypoint when the user taps it.
// Pass a single path parameter or component.
let id = 123
RouteLink(path: id) { Text("Label") }
// Go up a level.
RouteLink(path: "..") { Text("Label") }
// Pass an absolute path.
RouteLink(path: "/person/\(id)/company") { Text("Label") }
// Navigate the detail route. (when using the SplitNavigationView)
RouteLink(path: id, isDetail: true) { Text("Label") }
A waypoint is a single destination within a route. Examples of a waypoint might be a navigational stack item, ActionSheet, or TabView. The last waypoint of an active route is the current destination of the user. You can navigate relative to a waypoint using its navigate(to:inScene:isDetail:)
method.
@Environment(\.actionDispatcher) private var dispatch
@Environment(\.waypoint) private var waypoint
// Pass a single path parameter or component.
let id = 123
dispatch(waypoint.navigate(to: id))
// Go up a level.
dispatch(waypoint.navigate(to: ".."))
// Pass an absolute path.
dispatch(waypoint.navigate(to: "/person/\(id)/company"))
// Navigate the detail route.
dispatch(waypoint.navigate(to: id, isDetail: true) { Text("Label") }
New waypoints are initiated through the WaypointView. When initiating, the type of waypoint is provided to indicate how it may interact with the route's path.
// A constant value activates the waypoint if the current path component of the route matches the provided name.
WaypointView(.name("waypointName")) {
DestinationView()
}
waypoint.navigate(to: "/waypointName")
// A wildcard, parameterized waypoint that activates when a path component exists.
WaypointView(.parameter()) {
DestinationView()
}
// Access the parameter by type in the destination view.
struct DestinationView: View {
@WaypointParameter private var parameter: Int?
}
waypoint.navigate(to: "/123")
// A predicate function may be provided for more stringent validation.
WaypointView(.predicate({ Int($0) != nil })) {
DestinationView()
}
waypoint.navigate(to: "/123")
You can use the navigation actions directly if the above options aren't available. It also allows you to navigate by URL. This can be useful if the application has a custom URL scheme that launches a new scene for a specific view.
@Environment(\.actionDispatcher) private var dispatch
// Navigate to a URL. The first path component is the scene's name.
let url = URL(string: "/main/notes")!
dispatch(NavigationAction.navigate(to: url))
// Navigate with a master-detail URL. Use a url fragment to specify the detail route when applicable.
let url = URL(string: "/main/notes#/note/123")!
dispatch(NavigationAction.navigate(to: url)
// Pass a single path parameter or component.
dispatch(NavigationAction.navigate(to: "/notes", inScene: "main"))
// Go up a level.
dispatch(NavigationAction.navigate(to: "..", inScene: "main"))
The precedence of an active route path is based on its position in the view hierarchy. In cases where two or more routes share the same parent route, the higher-level route will be chosen. In the following example, the route that displays an alert will take precedence over the stack route when the relevant route path is set to "display-alert". Because the stack item takes a dynamic path parameter, any other value will active it instead.
List {
...
}
.stackItem(.parameter()) { NoteView() }
.alert(.name("display-alert")) { Alert(title: Text("Hello world!")) }
Use the View.stackItem(_:isDetail:content:)
to provide stackable waypoints for a NavigationView.
NavigationView {
AppSectionList()
.stackItem(.name("companies")) {
CompanyList()
.stackItem(parameter()) { CompanyDetail() }
}
.stackItem(.name("contacts")) {
ContactList()
.stackItem(parameter()) { ContactDetail() }
}
.stackItem(.name("settings")) {
Settings()
}
}
// In the body of the CompanyList view:
RouteLink(path: "/companies/\(company.id)") {
Text(company.name)
}
Use the isDetail
parameter to display a stack item on the detail route.
NavigationView {
NoteListContainer()
.stackItem(.parameter(), isDetail: true) { NoteEditorContainer() }
PlaceholderNote()
}
Use the Selection
view to provide routing support to TabViews and Lists. A Selection creates a parameterized Waypoint that updates itself when the selection binding changes. It supports selection bindings that take either a regular or optional type.
Selection(initialValue: "allMusic") { selection in
TabView(selection: selection) {
AllMusicContainer()
.tabItem(title: "All Music", image: "music.note")
.tag("allMusic")
AlbumsContainer()
.tabItem(title: "Albums", image: "rectangle.stack")
.tag("albums")
PlaylistsContainer()
.tabItem(title: "Playlists", image: "music.note.list")
.tag("playlists")
}
}