FlowStacks Documentation

repository·main·Indexed 20 days ago

https://github.com/johnpatrickmorgan/flowstacks

A SwiftUI navigation enhancement library that extends the NavigationStack API to support sheets and full-screen covers. It provides improved support for deep-linking and programmatic navigation via FlowNavigator, and allows for both shared and independent state when nesting stacks. Key components include FlowStack, FlowLink, FlowPath, and flowDestination.

Tokens
2.5K
Snippets
4
Records
7
Agent score
76%

What's inside FlowStacks

  1. How to nest FlowStacks using shared state

    main

    You can nest a child FlowStack within a parent coordinator by instantiating the child without its own data binding. In this mode, the child shares the parent's FlowPath as its source of truth.

    Key Behaviors:

    • Shared Path: Any type can be pushed onto the path. As long as a flowDestination modifier is declared for that type somewhere in the stack, the screen will be shown.
    • Unified Navigation: Both parent and child can push new routes. The parent's path will include all routes pushed by the child.
    • Global Reset: Calling goBackToRoot from the child will navigate all the way back to the parent's root screen.
    • Responsibility: The parent determines whether the child is shown with navigation or not.
  2. How FlowStacks works and its core concepts

    main

    FlowStacks is a replacement for SwiftUI's NavigationStack that extends navigation capabilities to include sheets and full-screen covers. It is designed to work on older versions of iOS, tvOS, watchOS, and macOS by translating a routes array into a hierarchy of nested NavigationLinks and presentation calls.

    Key API Mappings

    If you are familiar with SwiftUI's NavigationStack, the APIs are nearly identical, replacing 'Navigation' with 'Flow':

    • NavigationStack $\rightarrow$ FlowStack
    • NavigationLink $\rightarrow$ FlowLink
    • NavigationPath $\rightarrow$ FlowPath
    • navigationDestination $\rightarrow$ flowDestination

    Unlike standard SwiftUI navigation, when using FlowLink, you must specify a style to determine how the destination is presented:

    • .push: Standard push navigation.
    • .sheet: Presents the destination as a sheet.
    • .cover: Presents the destination as a full-screen cover.

    Note: For sheets and covers, you can pass withNavigation: true (e.g., .sheet(withNavigation: true)) if you want the presented view to be able to push further screens onto its own stack.

    FlowStack($path, withNavigation: true) {
      HomeView()
        .flowDestination(for: Int.self, destination: { number in
          NumberView(number: number)
        })
        .flowDestination(isPresented: $isShowingWelcome, style: .sheet) {
          Text("Welcome to FlowStacks!")
        }
    }
  3. How to nest FlowStacks with independent state

    main

    You can nest a child FlowStack that maintains its own independent state by providing it with its own data binding (either a FlowPath or a typed routes array). This is required if the parent is using a typed routes array (e.g., [Route<MyScreen>]).

    Key Behaviors:

    • Independent Navigation: The child has its own array of routes. It assumes full responsibility for navigation until it is removed from the parent's path.
    • Local Reset: Calling goBackToRoot from the child will only go back to the child's own root screen, not the parent's.
    • Conflict Prevention: To avoid navigation conflicts, the child coordinator must always be at the top of the parent's routes stack. If the parent attempts to push screens while the child is also pushing screens, navigation may fail or conflict.
    • Responsibility: The child determines whether its own root should be shown with navigation or not.
  4. Implement deep-linking with FlowStacks

    main
    FlowStacks solves the limitation in standard SwiftUI where multiple screens cannot be pushed in a single state update. When performing a deep-link, you can update the routes array with multiple destinations at once. FlowStacks will automatically break down the large update into a series of smaller, supported updates with necessary delays to ensure the navigation hierarchy is built correctly.
  5. Configure flowDestination with bindings for editable state

    main

    You can configure a flowDestination to work with a binding to its screen state within the routes array. This allows the destination view to directly modify the data stored in the navigation path.

    To enable this, add a $ before the screen argument in the flowDestination view-builder closure.

    Example: Simple Binding

    FlowStack($path, withNavigation: true) {
      FlowLink(value: 1, style: .push, label: { Text("Push '1'") })
        .flowDestination(for: Int.self) { $number in
          EditNumberScreen(number: $number) // Changes the value in the path
        }
    }

    Example: Binding to Enum Associated Values

    If you are using a typed array of enums (e.g., [Route<Screen>]), you can use the SwiftUINavigation library to extract bindings to specific enum cases:

    .flowDestination(for: Screen.self) { $screen in
      if let number = Binding(unwrapping: $screen, case: /Screen.number) {
        EditNumberScreen(number: number)
      }
    }
    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) {
                EditNumberScreen(number: number)
              } else if case let .greeting(greetingText) = screen {
                Text(greetingText)
              }
            }
        }
      }
    }
  6. Migrate from Router to FlowStack (v1.0+)

    main

    In version 1.0, the API was updated to align more closely with SwiftUI's NavigationStack. The primary changes involve decoupling state management from view building and changing how the root screen is handled.

    1. Decouple View Building

    Previously, the Router handled both state management and building destination views. In the new API, view building is decoupled into a separate function called flowDestination(for:destination:content:). You can call this on your root view to define how specific Screen cases are rendered.

    2. Handle the Root Screen

    The root screen is no longer part of the routes array.

    • Remove the root case: Remove the case representing your initial screen from your Screen enum.
    • Initialize the root view directly: Instead of including the root in the routes array, pass the root view directly into the FlowStack content closure.
    • Multiple root screens: If your flow requires switching between multiple possible root screens, it is recommended to split the logic into two separate flows, each with its own FlowStack, managed by a parent view.
    // New API Pattern (v1.0+)
    enum Screen: Hashable {
      case numberList
      case numberDetail(Int)
    }
    
    struct AppCoordinator: View {
      @State var routes: [Route<Screen>] = []
        
      var body: some View {
        FlowStack($routes, withNavigation: true) { 
          // 1. Root view is passed directly here
          HomeView()
            // 2. Destinations are decoupled using .flowDestination
            .flowDestination(for: Screen.self) { screen in
              switch screen {
              case .numberList:
                NumberListView()
              case .numberDetail(let number):
                NumberDetailView(number: number)
              }
            }
        }
      }
    }
  7. Use FlowNavigator to control navigation programmatically

    main

    A FlowNavigator object is provided via the SwiftUI environment, allowing you to trigger navigation actions (like pushing or popping) from anywhere in your view hierarchy without manual state manipulation.

    Accessing the Navigator

    Depending on how your FlowStack is backed, you access it using one of two types:

    1. For FlowPath-backed stacks:
      @EnvironmentObject var navigator: FlowPathNavigator
    2. For routes array-backed stacks (e.g., [Route<ScreenType>]):
      @EnvironmentObject var navigator: FlowNavigator<ScreenType>

    Available Methods

    MethodEffect
    push(_:)Pushes a new screen onto the stack.
    presentSheet(_:embedInNavigationView:)Presents a new screen as a sheet.
    presentCover(_:embedInNavigationView:)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.
    @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()
        }
      }
    }