SwiftfulRouting
Programmatic navigation for SwiftUI applications.
Install / Use
/learn @SwiftfulThinking/SwiftfulRoutingREADME
🚀 Learn how to build and use this package: https://www.swiftful-thinking.com/offers/REyNLwwH
SwiftfulRouting 🤙
Programmatic navigation for SwiftUI applications.
- ✅ Segues
- ✅ Alerts
- ✅ Modals
- ✅ Transitions
- ✅ Modules
How to use this package:
- 1️⃣ Read the docs below
- 2️⃣ Watch YouTube Tutorial
- 3️⃣ Practice with Sample Project
- 4️⃣ Test the Starter Project
Versioning:
- ➡️ iOS 17+ use version 6.0 or above
- ➡️ iOS 14+ use version 5.3.6
- ➡️ iOS 13+ use version 2.0.2
Quick Start (TLDR)
<details> <summary> Details (Click to expand) </summary> <br>Use a RouterView to replace NavigationStack in your SwiftUI code.
Before SwiftfulRouting:
NavigationStack {
MyView()
.navigationDestination()
.sheet()
.fullScreenCover()
.alert()
}
With SwiftfulRouting:
RouterView { _ in
MyView()
}
Use a router to perform actions.
struct MyView: View {
@Environment(\.router) var router
var body: some View {
Text("Hello, world!")
.onTapGesture {
router.showScreen { _ in
AnotherView()
}
}
}
}
All available methods in router are in AnyRouter.swift.
Examples:
router.showScreen()
router.showAlert()
router.showModal()
router.showTransition()
router.showModule()
router.dismissScreen()
router.dismissAlert()
router.dismissModal()
router.dismissTransition()
router.dismissModule()
</details>
How It Works
<details> <summary> Details (Click to expand) </summary> <br>As you segue to a new screen, the framework adds a set view modifiers to the root of the destination View that will support all potential navigation routes. This allows declarative code to behave as programmatic code, since the view modifiers are connected in advance. Screen destinations are erased to generic types, allowing the developer to determine the destination at the time of execution.
Version 6.0 adds many new features to the framework by implementing an internal RouterViewModel across the screen heirarchy that allows and screen's router to perform actions that affect the entire heirarchy. The solution introduces [AnyDestinationStack] which is a single array that holds bindings for all active segues in the heirarchy.
// Example of what an [AnyDestinationStack] might look like:
[
[.fullScreenCover]
[.push, .push, .push, .push]
[.sheet]
[]
]
In addition to adding a router to the Environment, every segue immedaitely returns a router in the View's closure. This allows the developer to have access to the screen's routing methods before the screen is created. Leave fully decouples routing logic from the View layer and is perfect for more complex app architectures, such as MVVM or VIPER.
RouterView { router in
MyView(router: router)
}
</details>
Setup
<details> <summary> Details (Click to expand) </summary> <br> Add the package to your Xcode project.https://github.com/SwiftfulThinking/SwiftfulRouting.git
Import the package.
import SwiftfulRouting
Add a RouterView at the top of your view heirarchy. A RouterView will embed your view into a NavigationStack and add modifiers to support all potential segues. This would replace an existing NavigationStack in your code.
Use a RouterView to replace NavigationStack in your SwiftUI code.
// Before SwiftfulRouting
NavigationStack {
MyView()
.navigationDestination()
.sheet()
.fullScreenCover()
.alert()
}
// With SwiftfulRouting
RouterView { _ in
MyView()
}
All child views have access to a Router in the Environment.
@Environment(\.router) var router
var body: some View {
Text("Hello, world!")
.onTapGesture {
router.showScreen(.push) { _ in
Text("Another screen!")
}
}
}
}
Instead of relying on the Environment, you can also pass the router directly into the child views.
RouterView { router in
MyView(router: router)
}
You can also use the returned router directly. A new router is created and added to the view heirarchy after each segue and are therefore unique to each screen. In the below example, the tap gesture on "View3" could call dismissScreen() from router2 or router3, which would have different behaviors. This is done on purpose and is further explained in the docs below!
RouterView { router1 in
Text("View 1")
.onTapGesture {
router1.showScreen(.push) { router2 in
Text("View 2")
.onTapGesture {
router2.showScreen(.push) { router3 in
Text("View3")
.onTapGesture {
router3.dismissScreen() // Dismiss View3
router2.dismissScreen() // Dismiss View2 and View 3
}
}
}
}
}
}
Refer to AnyRouter.swift to see all accessible methods.
</details>Setup (existing projects)
<details> <summary> Details (Click to expand) </summary> <br>In order to enter the framework's view heirarchy, you must wrap your content in a RouterView, which will add a NavigationStack by default.
Most apps should replace their existing NavigationStack with a RouterView, however, if you cannot remove it, you can add a RouterView but initialize it without a NavigationStack.
The framework uses the native SwiftUI navigation bar, so all related modifiers will still work.
RouterView(addNavigationStack: false) { router in
MyView()
.navigationBarHidden(true)
.toolbar {
}
}
</details>
Show Screens
<details> <summary> Details (Click to expand) </summary> <br>Router supports all native SwiftUI segues.
// Navigation destination
router.showScreen(.push) { _ in
Text("View2")
}
// Sheet
router.showScreen(.sheet) { _ in
Text("View2")
}
// FullScreenCover
router.showScreen(.fullScreenCover) { _ in
Text("View2")
}
Segue methods also accept AnyDestination as a convenience.
let screen = AnyDestination(segue: .push, destination: { router in
Text("Hello, world!")
})
router.showScreen(screen)
Segue to multiple screens at once. This will immediately trigger each screen in order, ending with the last screen displayed.
let screen1 = AnyDestination(segue: .push, destination: { router in
Text("Hello, world!")
})
let screen2 = AnyDestination(segue: .sheet, destination: { router in
Text("Another screen!")
})
let screen3 = AnyDestination(segue: .push, destination: { router in
Text("Third screen!")
})
router.showScreens(destinations: [screen1, screen2, screen3])
Use .sheetConfig() or .fullScreenCoverConfig() to for resizable sheets and backgrounds in new Environments.
let config = ResizableSheetConfig(
detents: [.medium, .large],
dragIndicator: .visible
)
router.showScreen(.sheetConfig(config: config)) { _ in
Text("Screen2")
}
let config = FullScreenCoverConfig(
background: .clear
)
router.showScreen(.fullScreenCoverConfig(config: config)) { _ in
Text("Screen2")
}
All segues have an onDismiss method.
router.showScreen(.push, onDismiss: {
// dismiss action
}, destination: { _ in
Text("Hello, world!")
})
Fully customize each segue!
let screen = AnyDestination(
id: "profile_screen", // id of screen (used for analytics)
segue: .fullScreenCover, // segue option
location: .insert, // where to add screen within the view heirarchy
animates: true, // animate the segue
transitionBehavior: .keepPrevious, // transition behavior (only relevant for showTransition methods)
onDismiss: {
// Do something when screen dismisses
},
destination: { _ in
Text("ProfileView")
}
)
Additional convenience methods:
router.showSafari {
URL(string: "https://www.apple.com")
}
</details>
Dismiss Screens
<details> <summary> Details (Click to expand) </summary> <br>Dismiss one screen.
router.dismissScreen()
You can also use the native SwiftUI method.
@Environment(\.dismiss) var dismiss
Dismiss screen at id.
router.dismissScreen(id: "x")
Dismiss screens back to, but not including, id.
router.dismissScreen(upToScreenId: "x")
Dismiss a specific number of screens.
router.dismissScreens(count: 2)
Dismiss all .push segues on the NavigationStack of the current screen.
router.dismissPushStack()
Dismiss screen environment (ie. the closest .sheet or .fullScreenCover to this screen).
router.dismissEnvironment()
Dismiss
Related Skills
node-connect
346.8kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
107.6kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
346.8kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
346.8kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
