Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/SwiftfulThinking/SwiftfulRouting
Programmatic navigation for SwiftUI applications.
https://github.com/SwiftfulThinking/SwiftfulRouting
ios swift swiftui
Last synced: 3 months ago
JSON representation
Programmatic navigation for SwiftUI applications.
- Host: GitHub
- URL: https://github.com/SwiftfulThinking/SwiftfulRouting
- Owner: SwiftfulThinking
- License: mit
- Created: 2022-04-30T20:53:10.000Z (over 2 years ago)
- Default Branch: main
- Last Pushed: 2024-08-02T18:51:54.000Z (3 months ago)
- Last Synced: 2024-08-03T17:21:13.121Z (3 months ago)
- Topics: ios, swift, swiftui
- Language: Swift
- Homepage: https://www.swiftful-thinking.com/
- Size: 172 KB
- Stars: 387
- Watchers: 9
- Forks: 27
- Open Issues: 3
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# SwiftfulRouting 🤙
SwiftfulRouting is a native, declarative framework that enables programmatic navigation in SwiftUI applications.
- Sample project: https://github.com/SwiftfulThinking/SwiftfulRoutingExample
- YouTube Tutorial: https://www.youtube.com/watch?v=zKfhv-Yds4g&list=PLwvDm4VfkdphPRGbtiY-X3IZsUXFi6595&index=6## How It Works
Details (Click to expand)
Routers based on programatic code do not declare the view heirarchy in advance, but rather at the time of execution. However, SwiftUI is declarative, and so we must declare the view heirarchy in advance. The solution herein is to convert SwiftUI's declarative code to behave as programmatic code by connecting view modifiers to support the routing in advance.
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. The modifiers are based on generic and/or type-erased destinations, which maintains a declarative view heirarchy while allowing the developer to still determine the destination at the time of execution.
- The ViewModifiers are in `RouterView.swift -> body`.
- Accessible routing methods are in `AnyRouter.swift`.
- Refer to the sample project for example implementations, UI Tests and sample MVC, MVVM and VIPER design patterns.Sample project: https://github.com/SwiftfulThinking/SwiftfulRoutingExample
## Setup
Details (Click to expand)
Add the package to your Xcode project.```
https://github.com/SwiftfulThinking/SwiftfulRouting.git
```Import the package
```swift
import SwiftfulRouting
```Add a `RouterView` at the top of your view heirarchy. A `RouterView` will embed your view into a Navigation heirarchy and add modifiers to support all potential segues.
```swift
struct ContentView: View {
var body: some View {
RouterView { _ in
MyView()
}
}
}
```All child views have access to a `Router` in the `Environment`.
```swift
@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 may also pass the `Router` directly into the child views. This allows the `Router` to be fully decoupled from the View (for more complex app architectures).
```swift
RouterView { router in
ContentView(router: router)
.onTapGesture {
router.showScreen(.push) { router2 in
Text("View2")
.onTapGesture {
router2.showScreen(.push) { router3 in
Text("View3")
}
}
}
}
}
```A new Router is created and added to the view heirarchy after each Segue. Refer to `AnyRouter.swift` to see all accessible methods.
## Setup (existing projects)
Details (Click to expand)
In order to enter the framework's view heirarchy, you must wrap your content in a RouterView. By default, your view will be wrapped in with navigation stack (iOS 16+ uses a NavigationStack, iOS 15 and below uses NavigationView).
- If your view is already within a navigation heirarchy, set `addNavigationView` to `FALSE`.
- If your view is already within a NavigationStack, use `screens` to bind to the existing stack path.
- The framework uses the native SwiftUI navigation bar, so all related modifiers will still work.```swift
RouterView(addNavigationView: false, screens: $existingStack) { router in
MyView(router: router)
.navigationBarHidden(true)
.toolbar {
}
}
```## Show Screens
Details (Click to expand)
Router supports all native SwiftUI segues.
```swift
// NavigationLink
router.showScreen(.push) { _ in
Text("View2")
}// Sheet
router.showScreen(.sheet) { _ in
Text("View2")
}// FullScreenCover
router.showScreen(.fullScreenCover) { _ in
Text("View2")
}
```Segue methods also accept `AnyRoute` as a convenience, which make it easy to pass the `Route` around your code.
```swift
let route = AnyRoute(.push, destination: { router in
Text("Hello, world!")
})
router.showScreen(route)
```All segues have an onDismiss method.
```swift
router.showScreen(.push, onDismiss: {
// dismiss action
}, destination: { _ in
Text("Hello, world!")
})
let route = AnyRoute(.push, onDismiss: {
// dismiss action
}, destination: { _ in
Text("Hello, world!")
})
router.showScreen(route)
```iOS 16+ uses NavigationStack, which supports pushing multiple screens at once.
```swift
let route1 = PushRoute(destination: { router in
Text("View1")
})
let route2 = PushRoute(destination: { router in
Text("View2")
})
let route3 = PushRoute(destination: { router in
Text("View3")
})
router.pushScreenStack(destinations: [route1, route2, route3])
```iOS 16+ also supports resizable sheets.
```swift
router.showResizableSheet(sheetDetents: [.medium, .large], selection: nil, showDragIndicator: true) { _ in
Text("Hello, world!)
}
```Additional convenience methods:
```swift
router.showSafari {
URL(string: "https://www.apple.com")
}
```## Enter Screen Flows
Details (Click to expand)
Screen "flows" are new way to support dynamic routing in your application. When you enter a "screen flow", you add an array of `Routes` to the heirarchy. The application will immediately segue to the first screen, and then set the remaining screens into a queue.
```swift
router.enterScreenFlow([
AnyRoute(.fullScreenCover, destination: screen1),
AnyRoute(.push, destination: screen2),
AnyRoute(.push, destination: screen3),
AnyRoute(.push, destination: screen4),
])
```This allows the developer to set multiple future segues at once, without requiring screen-specific code in each child view. Each child view's routing logic is simple as "try to go to next screen".
```swift
do {
try router.showNextScreen()
} catch {
// There is no next screen set in the flow
// Dismiss the flow (see below dismiss methods) or do something else
}
```Benefits of using a "flow":
- **Simiplified Logic:** In most applications, the routing logic is tightly coupled to the View (ie. when you create a screen, you declare in code exactly what the next screen must be). Now, you can build a screen without having to worry about routing at all. Simply support "go to next screen" or "dismiss flow" (see dismissal code below).
- **AB Tests:** Each user can see a unique flow of screens in your app, and you don't have to write 'if-else' logic within every child view.
- **High-Level Control**: You can control the entire flow from one method, which will be closer to the business logic of your app, rather than within the View itself.
- **Flows on Flows**: Flows are fully dynamic, meaning you can enter flows from within flows and can dismiss screens within flows (back-forward-back) without corrupting the flow.
## Dismiss Screens
Details (Click to expand)
Dismiss one screen. You can also dismiss a screen using native SwiftUI code, including swipe-back gestures or `presentationMode`.
```swift
router.dismissScreen()
```Dismiss all screens pushed onto the stack. This dismisses every "push" (NavigationLink) on the screen's Navigation Stack. This does not dismiss `sheet` or `fullScreenCover`.
```swift
router.dismissScreenStack()
```Dismiss screen environment. This dismisses the screen's root environment (if there is one to dismiss), which is the closest 'sheet' or `fullScreenCover` below the call-site.
```swift
router.dismissEnvironment()
```For example, if you entered the following screen flow and you called `dismissEnvironment` from any of the child views, it would dismiss the `fullScreenCover`, which in-turn dismisses every view displayed on that Environment.
```swift
router.enterScreenFlow([
AnyRoute(.fullScreenCover, destination: screen1),
AnyRoute(.push, destination: screen2),
AnyRoute(.push, destination: screen3),
AnyRoute(.push, destination: screen4),
])
```Logic for dismissing a "Flow" can generally look like:
```swift
do {
try router.showNextScreen()
} catch {
router.dismissEnvironment()
}
```Or convenience method:
```swift
router.showNextScreenOrDismissEnvironment()
```Copy and paste this code into your project to enable swipe back gestures. This is not included in the SwiftUI framework by default and therefore is not automatically included herein.
```swift
extension UINavigationController: UIGestureRecognizerDelegate {
override open func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = self
}
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return viewControllers.count > 1
}
}
```## Alerts
Details (Click to expand)
Router supports native SwiftUI alerts.
```swift
// Alert
router.showAlert(.alert, title: "Title goes here", subtitle: "Subtitle goes here!") {
Button("OK") {}
Button("Cancel") {
}
}// Confirmation Dialog
router.showAlert(.confirmationDialog, title: "Title goes here", subtitle: "Subtitle goes here!") {
Button("A") {
}
Button("B") {
}
Button("C") {
}
}
```Dismiss an alert.
```swift
router.dismissAlert()
```Additional convenience methods:
```swift
router.showBasicAlert(text: "Error")
```## Modals
Details (Click to expand)
Router also supports any modal transition, which displays above the current content. Customize transition, animation, background color/blur, etc. See sample project for example implementations.
```swift
router.showModal(transition: .move(edge: .top), animation: .easeInOut, alignment: .top, backgroundColor: nil, useDeviceBounds: true) {
Text("Sample")
.onTapGesture {
router.dismissModal()
}
}
```You can display multiple modals simultaneously. Modals have an optional ID field, which can later be used to dismiss the modal.
```swift
router.showModal(id: "top1") {
Text("Sample")
}// Dismiss top-most modal
router.dismissModal()// Dismiss modal by ID
router.dismissModal(id: "top1")// Dismiss all modals
router.dismissAllModals()```
Additional convenience methods:
```swift
router.showBasicModal {
Text("Sample")
.onTapGesture {
router.dismissModal()
}
}
```## Contribute 🤓
Details (Click to expand)
Community contributions are encouraged! Please ensure that your code adheres to the project's existing coding style and structure. Most new features are likely to be derivatives of existing features, so many of the existing ViewModifiers and Bindings should be reused.
- [Open an issue](https://github.com/SwiftfulThinking/SwiftfulRouting/issues) for issues with the existing codebase.
- [Open a discussion](https://github.com/SwiftfulThinking/SwiftfulRouting/discussions) for new feature requests.
- [Submit a pull request](https://github.com/SwiftfulThinking/SwiftfulRouting/pulls) when the feature is ready.Upcoming features:
- [x] Support multiple Modals per screen
- [ ] Add `showModule` support, for navigating between parent-level RouterView's
- [ ] Support VisionOS