Contents

johnpatrickmorgan/flowstacks

This package takes SwiftUI's familiar and powerful `NavigationStack` API and gives it superpowers, allowing you to use the same API not just for push navigation, but also for presenting sheets and full-screen covers. And because it's implemented using the navigation APIs availabl

Example

<details> <summary>Click to expand an example</summary>

import FlowStacks
import SwiftUI

struct ContentView: View {
  @State var path = FlowPath()
  @State var isShowingWelcome = false

  var body: some View {
    FlowStack($path, withNavigation: true) {
      HomeView()
        .flowDestination(for: Int.self, destination: { number in
          NumberView(number: number)
        })
        .flowDestination(for: String.self, destination: { text in
          Text(text)
        })
        .flowDestination(isPresented: $isShowingWelcome, style: .sheet) {
          Text("Welcome to FlowStacks!")
        }
    }
  }
}

struct HomeView: View {
  @EnvironmentObject var navigator: FlowPathNavigator
  
  var body: some View {
    List {
      ForEach(0 ..< 10, id: \.self) { number in
        FlowLink(value: number, style: .sheet(withNavigation: true), label: { Text("Show \(number)") })
      }
      Button("Show 'hello'") {
        navigator.push("Hello")
      }
    }
    .navigationTitle("Home")
  }
}

struct NumberView: View {
  @EnvironmentObject var navigator: FlowPathNavigator
  let number: Int

  var body: some View {
    VStack(spacing: 8) {
      Text("\(number)")
      FlowLink(
        value: number + 1,
        style: .push,
        label: { Text("Show next number") }
      )
      Button("Go back to root") {
        navigator.goBackToRoot()
      }
    }
    .navigationTitle("\(number)")
  }
}

</details>

Additional features

As well as replicating the standard features of the new NavigationStack APIs, some helpful utilities have also been added.

FlowNavigator

A FlowNavigator object is available through the environment, giving access to the current routes array and the ability to update it via a number of convenience methods. The navigator can be accessed via the environment, e.g. for a FlowPath-backed stack:

@EnvironmentObject var navigator: FlowPathNavigator

Or for a FlowStack backed by a routes array, e.g. [Route<ScreenType>]:

@EnvironmentObject var navigator: FlowNavigator<ScreenType>

Here's an example of a FlowNavigator in use:

@EnvironmentObject var navigator: FlowNavigator<ScreenType>

var body: some View {
  VStack {
    Button("View detail") {
      navigator.push(.detail)
    }
    Button("Go back to profile") {
      navigator.goBackTo(.profile)
    }
    Button("Go back to root") {
      navigator.goBackToRoot()
    }
  }
}

Convenience methods

When interacting with a FlowNavigator (and also the original FlowPath or routes array), a number of convenience methods are available for easier navigation, including:

| Method | Effect | |--------------|---------------------------------------------------| | push | Pushes a new screen onto the stack. | | presentSheet | Presents a new screen as a sheet.† | | presentCover | Presents a new screen as a full-screen cover.† | | goBack | Goes back one screen in the stack. | | goBackToRoot | Goes back to the very first screen in the stack. | | goBackTo | Goes back to a specific screen in the stack. | | pop | Pops the current screen if it was pushed. | | dismiss | Dismisses the most recently presented screen. |

† Pass embedInNavigationView: true if you want to be able to push screens from the presented screen.

Deep-linking

Before the NavigationStack APIs were introduced, SwiftUI did not support pushing more than one screen in a single state update, e.g. when deep-linking to a screen multiple layers deep in a navigation hierarchy. FlowStacks works around this limitation: you can make any such changes, and the library will, behind the scenes, break down the larger update into a series of smaller updates that SwiftUI supports, with delays if necessary in between.

Bindings

The flow destination can be configured to work with a binding to its screen state in the routes array, rather than just a read-only value - just add $ before the screen argument in the flowDestination function's view-builder closure. The screen itself can then be responsible for updating its state within the routes array, e.g.:

import SwiftUINavigation

struct BindingExampleCoordinator: View {
  @State var path = FlowPath()
    
  var body: some View {
    FlowStack($path, withNavigation: true) {
      FlowLink(value: 1, style: .push, label: { Text("Push '1'") })
        .flowDestination(for: Int.self) { $number in
          EditNumberScreen(number: $number) // This screen can now change the number stored in the path.
        }
    }
  }

If you're using a typed Array of routes, you're probably using an enum to represent the screen, so it might be necessary to further extract the associated value for a particular case of that enum as a binding. You can do that using the SwiftUINavigation library, which includes a number of helpful Binding transformations for optional and enum state, e.g.:

<details> <summary>Click to expand an example of using a Binding to a value in a typed Array of enum-based routes</summary>

import FlowStacks
import SwiftUI
import SwiftUINavigation

enum Screen: Hashable {
  case number(Int)
  case greeting(String)
}

struct BindingExampleCoordinator: View {
  @State var routes: Routes<Screen> = []

  var body: some View {
    FlowStack($routes, withNavigation: true) {
      HomeView()
        .flowDestination(for: Screen.self) { $screen in
          if let number = Binding(unwrapping: $screen, case: /Screen.number) {
            // Here `number` is a `Binding<Int>`, so `EditNumberScreen` can change its
            // value in the routes array.
            EditNumberScreen(number: number)
          } else if case let .greeting(greetingText) = screen {
            // Here `greetingText` is a plain `String`, as a binding is not needed.
            Text(greetingText)
          }
        }
    }
  }
}

struct HomeView: View {
  @EnvironmentObject var navigator: FlowPathNavigator

  var body: some View {
    VStack {
      FlowLink(value: Screen.number(42), style: .push, label: { Text("Show Number") })
      FlowLink(value: Screen.greeting("Hello world"), style: .push, label: { Text("Show Greeting") })
    }
  }
}

struct EditNumberScreen: View {
  @Binding var number: Int

  var body: some View {
    Stepper(
      label: { Text("\(number)") },
      onIncrement: { number += 1 },
      onDecrement: { number -= 1 }
    )
  }
}

</details>

Child flow coordinators

FlowStacks are designed to be composable, so that you can have multiple flow coordinators, each with its own FlowStack, and you can present or push a child coordinator from a parent. See Nesting FlowStacks for more info.

How does it work?

The library works by translating the array of routes into a hierarchy of nested NavigationLinks and presentation calls, expanding on the technique used in NavigationBackport.

Migrating from earlier versions

Please see the migration docs.


[[ko-fi]](https://ko-fi.com/T6T114GWOT)

Package Metadata

Repository: johnpatrickmorgan/flowstacks

Default branch: main

README: README.md