SkillAgentSearch skills...

SwiftfulRouting

Programmatic navigation for SwiftUI applications.

Install / Use

/learn @SwiftfulThinking/SwiftfulRouting
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

🚀 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:

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

View on GitHub
GitHub Stars957
CategoryDevelopment
Updated17h ago
Forks68

Languages

Swift

Security Score

100/100

Audited on Apr 2, 2026

No findings