Flow Navigation is a declarative navigation system for SwiftUI that enables linear, structured, type-safe flows.
It provides a seamless way to set up screens, pass data between them, and handle navigation logic dynamically.
Using Flow Navigation, you can define your linear flow simply as:
Flow {
IntroductionScreen()
UserSelectionScreen(users: users)
FlowReader { proxy in
// Read data typesafe from the user selection screen
let selection = try proxy.data(for: UserSelectionScreen.self)
return ConfirmationScreen(user: selection)
}
}
Important
This framework is currently in beta and has not been battle-tested.
I am actively developing an app to dogfood it, but feel free to use it and report any issues.
Your feedback is highly valuable in improving Flow Navigation! 🚀
Each screen conforms to FlowScreen
.
Each screen has a static screenId and defines both its body and Output type. The body method receives a control parameter, which is used to advance the flow.
struct IntroductionScreen: FlowScreen {
static let screenId = "IntroductionScreen"
func body(control: FlowScreenControl<Void>) -> some View {
Text("This screen has no output, hence the Void type.")
Text("It is just an intro screen.")
Button("Continue") {
Task {
await control.next() // Advance to the next screen
}
}
}
}
Screens can also advance with an output.
struct UserSelectionScreen: FlowScreen {
static let screenId = "UserSelectionScreen"
let users: [User]
func body(control: FlowScreenControl<UUID>) -> some View {
List(users) { user in
Button(user.name) {
Task {
await control.next(user.id) // Pass selected user ID and advance
}
}
}
}
}
Note
It can be a good idea to have a dedicated TaskButton view to handle the FlowScreenControl
.
FlowNavigation does not ship with one.
... but its easely created
import SwiftUI
struct TaskButton<Label>: View where Label: View {
@State private var runTask = false
private let label: Label
private let action: @Sendable () async -> Void
init(
@_inheritActorContext _ action: @escaping @Sendable () async -> Void,
@ViewBuilder label: () -> Label
) {
self.label = label()
self.action = action
}
init<S: StringProtocol>(
_ title: S,
@_inheritActorContext _ action: @escaping @Sendable () async -> Void
) where Label == Text {
self.label = Text(title)
self.action = action
}
var body: some View {
Button {
runTask = true
} label: {
label
}
.task(id: runTask, priority: .userInitiated) {
guard runTask else { return }
await action()
runTask = false
}
}
}
A Flow defines the sequence of screens and automatically progresses when a screen completes. Use the flow as any other SwiftUI view. When a flow is finished, it will call the dismiss environment action, so its suitable for being shown in sheets.
var body: some View {
Button("Select user") {
showSelectUserFlow = true
}
.sheet(isPresented: $showSelectUserFlow) {
Flow {
IntroductionScreen()
UserSelectionScreen(users: users)
FlowReader { proxy in
let selection = try proxy.data(for: UserSelectionScreen.self)
return ConfirmationScreen(user: selection)
}
}
}
}
The flow progresses linearly as follows:
- The
IntroductionScreen
is shown first. - The
UserSelectionScreen
allows the user to select a user, passing the selected ID to the flow. FlowReader
reads the selected user’s ID and passes it to theConfirmationScreen
.
To use this package in a SwiftPM project, you need to set it up as a package dependency:
// swift-tools-version:6.0
import PackageDescription
let package = Package(
name: "MyPackage",
dependencies: [
.package(url: "https://github.com/magnuskahr/swiftui-flow-navigation", from: "0.11.3")
],
targets: [
.target(
name: "MyTarget",
dependencies: [
.product(name: "FlowNavigation", package: "swiftui-flow-navigation")
]
)
]
)