A SwiftUI navigation library for type-safe stack navigation, sheet presentation, and tab-based layouts.
- iOS 18.0+
- Swift 6.2+
Add Flow to your project via Xcode (File → Add Package Dependencies) or by adding it to your Package.swift:
dependencies: [
.package(url: "https://github.com/pedro0x53/flow", from: "2.0.0")
]Flow is organized into three layers:
| Layer | Purpose |
|---|---|
| Horizontal Flow | Stack navigation (push/pop) with NavigationStack |
| Vertical Flow | Sheet presentation |
| Context Flow | Tab-based navigation |
Stack navigation with FlowStack, FlowStackCoordinator, and FlowRoute.
Conform to FlowRoute (or FlowView if the route is also a View):
import Flow
import SwiftUI
// Option 1: FlowView — route and view are the same
struct ProfileView: FlowView {
var body: some View {
Text("Profile")
}
}
struct SettingsView: FlowView {
var body: some View {
Text("Settings")
}
}
// Option 2: Enum routes with associated values (for path persistence)
enum AppRoute: FlowRoute {
case profile(userId: String)
case settings
@ViewBuilder
var destination: some View {
switch self {
case .profile(let userId):
ProfileDetailView(userId: userId)
case .settings:
SettingsView()
}
}
}struct ContentView: View {
@State var coordinator = FlowStackCoordinator()
var body: some View {
FlowStack(coordinator: coordinator) {
VStack(spacing: 16) {
Text("Home")
.destination(ProfileView.self)
.destination(SettingsView.self)
Button("Go to Profile") {
coordinator.push(ProfileView())
}
Button("Go to Settings") {
coordinator.push(SettingsView())
}
}
}
}
}// Push
coordinator.push(ProfileView())
// Pop one screen
coordinator.pop()
// Pop multiple screens
coordinator.pop(2)
// Pop to root
coordinator.popToRoot()Sheet presentation with FlowPresenter and the sheet(_:_:) modifier.
import Flow
import SwiftUI
struct ContentView: View {
@State var presenter = FlowPresenter()
var body: some View {
Button("Present Sheet") {
presenter.present()
}
.sheet(presenter) {
Text("Sheet Content")
}
}
}struct DetailView: View {
@Environment(FlowPresenter.self) var presenter
var body: some View {
VStack {
Text("Detail")
Button("Dismiss") {
presenter.dismiss()
}
}
}
}
// With callbacks
let presenter = FlowPresenter()
presenter.setOnPresent { print("Sheet presented") }
presenter.setOnDismiss { print("Sheet dismissed") }Tab navigation with FlowTabView, FlowTabCoordinator, and FlowTab.
import Flow
import SwiftUI
enum MyTabs: String, CaseIterable, FlowTabs {
case home
case search
case profile
static var tabs: [MyTabs] { MyTabs.allCases }
var title: String {
switch self {
case .home: return "Home"
case .search: return "Search"
case .profile: return "Profile"
}
}
var image: FlowTabImage {
switch self {
case .home: return .system("house")
case .search: return .system("magnifyingglass")
case .profile: return .system("person")
}
}
@ViewBuilder
var destination: some View {
switch self {
case .home: HomeTab()
case .search: SearchTab()
case .profile: ProfileTab()
}
}
}@main
struct MyApp: App {
let tabCoordinator = FlowTabCoordinator<MyTabs>(tab: .home)
var body: some Scene {
WindowGroup {
FlowTabView(tabCoordinator: tabCoordinator)
}
}
}struct HomeTab: View {
@Environment(FlowTabCoordinator<MyTabs>.self) var tabCoordinator
var body: some View {
Button("Go to Profile") {
tabCoordinator.select(tab: .profile)
}
}
}Tabs with nested stack and sheet flows:
struct HomeTab: FlowView {
@State var coordinator = FlowStackCoordinator()
@State var sheetPresenter = FlowPresenter()
var body: some View {
FlowStack(coordinator: coordinator) {
VStack {
Text("Home")
.destination(DetailView.self)
Button("Push Detail") {
coordinator.push(DetailView())
}
Button("Present Sheet") {
sheetPresenter.present()
}
}
}
.sheet(sheetPresenter) {
Text("Sheet Content")
}
}
}Flow coordinators conform to Codable to support path persistence and restoration. The FlowRoute protocol does not require Codable—use it when you need persistable navigation.
- Path persistence — Encoding the coordinator (e.g. for state restoration) only succeeds when every value in the path is
Codable. - Enum routes — Raw-value or associated-value enums are typically
Codableand work well:
enum AppRoute: Int, FlowRoute {
case home
case profile
case settings
var destination: some View { /* ... */ }
}- FlowView — Struct views conform to
Codableonly if their stored properties areCodable. Views with@State,@StateObject, or other non-Codable properties cannot participate in path persistence. pushoverload — Usepushwith aCodableroute to get compile-time assurance that the path can be encoded.
FlowStackCoordinator uses NavigationPath.CodableRepresentation for encoding and decoding the stack. If the path contains any non-Codable value, encoding throws EncodingError.invalidValue with "Path contains non-Codable values".
This project is licensed under the MIT License.