Swift Navigation

repository·main·Indexed 25 days ago

https://github.com/pointfreeco/swift-navigation

A foundational library providing primitives for building state-driven navigation and observation tools across SwiftUI, UIKit, AppKit, and non-Apple platforms such as Windows, Linux, and Wasm. It includes core components like `observe`, `UIBinding`, `UITransaction`, and `UINavigationPath`, and provides state representations for UI elements via `AlertState`, `ButtonState`, and `ConfirmationDialogState`.

Tokens
12.4K
Snippets
24
Records
86
Agent score
76%

What's inside Swift Navigation

  1. Overview of Swift Navigation

    main

    Swift Navigation is a foundational library providing tools for building state-driven navigation and state management APIs across Apple platforms (SwiftUI, UIKit, AppKit) and non-Apple platforms (Windows, Linux, Wasm).

    It provides core primitives that serve as the building blocks for more specialized libraries like SwiftUINavigation and UIKitNavigation.

    Key core components include:

    • observe: Minimally observes changes in a model to trigger updates.
    • UIBinding: Provides two-way binding to connect navigation and UI components to an observable model.
    • UITransaction: Associates animations and metadata with state changes.
    • UINavigationPath: A type-erased stack of data used to describe stack-based navigation.
  2. Overview of SwiftNavigation

    main

    SwiftNavigation is a foundational library providing tools for state management and navigation across all Swift platforms (SwiftUI, UIKit, AppKit, Windows, Linux, Wasm, etc.). It provides the primitives necessary to build advanced navigation systems.

    Core components include:

    • Observation: Tools like observe(isolation:_:)-9xf99 for minimally observing model changes.
    • Binding: UIBinding for two-way binding between navigation/UI components and observable models.
    • Navigation Primitives: UINavigationPath for type-erased stack-based navigation and UITransaction for associating animations or metadata with state changes.
  3. How state-driven navigation works in UIKitNavigation

    main

    Unlike standard UIKit navigation which is "fire-and-forget" (invoking a method to trigger navigation without representing the event in your state), UIKitNavigation allows you to drive navigation from an observable state.

    By modeling destinations as an enum and using the @CasePathable macro, you can bind your view controller's navigation actions to changes in your model. This ensures that:

    1. The model and view stay in sync: setting a state value triggers navigation, and dismissing a view controller automatically sets the state back to nil.
    2. Deep-linking is simplified: you can navigate to any part of the app by simply constructing the appropriate state.
    3. Testing is easier: the navigation state is part of your feature's logic rather than being hidden in the view hierarchy.
    @CasePathable
    enum Destination {
      case addItem(AddItemModel)
      case deleteItemAlert
      case editItem(EditItemModel)
    }
    
    @Observable
    class FeatureModel {
      var destination: Destination?
    }
  4. Attach data to mutations with UITransaction

    main

    To associate animations, metadata, or other transient data with state changes, use UITransaction. This allows mutations to carry extra context that the UI can react to (e.g., triggering a specific animation during a state transition).

    Key types:

    • UITransaction: The container for associated data.
    • UITransactionKey: Used to identify specific pieces of data within a transaction.
    • withUITransaction(_:_:) and withUITransaction(_:_:_:): Functions to wrap state mutations within a transaction.
  5. Drive navigation to multiple destinations using Enums and CasePaths

    main

    When a feature can navigate to multiple, mutually exclusive screens, use an enum to represent the destination state. By using the @CasePathable macro (from the CasePaths library), you can create bindings to specific enum cases to drive navigation.

    Setup

    1. Define an enum with @CasePathable.
    2. Hold an optional instance of that enum in your state.
    3. Use the specialized NavigationLink or navigationDestination overloads that accept a binding to a specific case.
    @CasePathable
    enum Destination {
      case counter(Int)
      case text(String)
    }
    
    @State var destination: Destination?
    
    // ...
    
    NavigationLink(item: $destination.counter) { isActive in
      destination = isActive ? .counter(42) : nil
    } destination: { $number in
      CounterView(number: $number)
    } label: {
      Text("Go to counter")
    }
  6. Drive navigation from state on non-Apple platforms

    main

    You can drive UI elements (like alerts or dialogs) from your application state on cross-platform targets. For example, you can use the alert(isPresented:_:)-9xf99 pattern to present an alert based on a boolean property in your model, which can then be implemented via platform-specific APIs (like the browser's alert or a custom DOM element).

    alert(isPresented: $model.isShowingErrorAlert) { 
      "Something went wrong"
    }
  7. Manage UI state with specialized state types

    main

    The library provides specialized state types to represent common UI elements, making it easier to manage the presentation of transient UI components:

    • TextState: For managing text-based UI state.
    • AlertState: For managing the presentation of alerts.
    • ConfirmationDialogState: For managing confirmation dialogs.
    • ButtonState: For managing button-related states.
  8. Drive SwiftUI navigation from enum state

    main

    To avoid the complexity and invalid states of using multiple optional properties for navigation (e.g., multiple sheets or alerts being active simultaneously), you can use an enum to represent a single, mutually exclusive destination.

    By annotating your destination enum with the @CasePathable macro from SwiftNavigation, you can use dot-syntax to derive bindings for SwiftUI modifiers like .sheet, .alert, and .navigationDestination.

    Note: To use these tools, you must depend on the SwiftNavigation package and import the SwiftUINavigation library.

    import SwiftNavigation 
    import SwiftUINavigation 
    
    @Observable
    class FeatureModel {
      var destination: Destination?
    
      @CasePathable
      enum Destination {
        case addItem(AddItemModel)
        case deleteItemAlert
        case editItem(EditItemModel)
      }
    }
    
    // In your SwiftUI View:
    .sheet(item: $model.destination.addItem) { addItemModel in
      AddItemView(model: addItemModel)
    }
    .alert("Delete?", isPresented: Binding($model.destination.deleteItemAlert)) {
      Button("Yes", role: .destructive) { /* ... */ }
      Button("No", role: .cancel) {}
    }
    .navigationDestination(item: $model.destination.editItem) { editItemModel in
      EditItemView(model: editItemModel)
    }
  9. Test alerts and dialogs via Equatable state

    main

    Because AlertState, TextState, ButtonState, and the associated action enums are all Equatable, you can write unit tests to verify that the correct alert is presented with the correct content.

    Example test pattern:

    1. Trigger the method that sets the alert state.
    2. Assert that model.alert is not nil.
    3. Assert that model.alert?.title matches the expected TextState.
    4. Simulate a button tap by calling the action handler with the expected action.
    5. Assert that the resulting side effect (e.g., deletion) occurred.
    func testDelete() {
      let model = FeatureModel(/* ... */)
    
      model.deleteButtonTapped()
      XCTAssertEqual(model.alert?.title, TextState("Are you sure?"))
    
      model.alertButtonTapped(.confirmDelete)
      // Assert that deletion actually occurred.
    }
  10. Create and share state with UIBindable and UIBinding

    main

    SwiftNavigation provides mechanisms for two-way binding to connect UI components to an observable model:

    • UIBindable: Used for creating and sharing state.
    • UIBinding: Provides the two-way binding mechanism between navigation/UI and the model.
    • CaseBindable / CaseBindable(): Specialized tools for handling state related to enums/cases.
  11. Understand the concept of state-driven navigation

    main

    In Swift Navigation, navigation is modeled as a change in application "mode" driven by state. Instead of imperatively telling a view controller to push or present, you describe navigation as an optional piece of state.

    How it works

    • Presentation: When the navigation state changes from nil to a non-nil value, a screen is presented (e.g., via drill-down, modal, sheet, or full-screen cover).
    • Dismissal: When the state changes from a non-nil value back to nil, the screen is dismissed.

    Benefits of this approach

    • Model-UI Sync: Guarantees the visual UI always matches your application state.
    • Deep Linking: Enables deep linking by allowing you to jump to any application state simply by constructing the corresponding data.
    • Testability: Allows for unit testing navigation logic by asserting state changes, avoiding slow and flaky UI tests.
    • Platform Agnostic: This mental model applies to SwiftUI, UIKit, AppKit, and even non-Apple platforms like Windows, Linux, or Wasm.