/tca-example-recursive-navigation

An approach to recursive navigation with The Composable Architecture.

Primary LanguageSwiftMIT LicenseMIT

TCA Recursive Navigation

An experiment of recursive navigation using The Composable Architecture.

Dependencies

Novel Ideas

  • View.navigation and View.sheet modifiers use an Action to dismiss instead of an onDismiss callback in Composable Presentation.
  • View.presentation unifies all types of presentation, allowing the type to be chosen at runtime (Navigation vs Sheet for example). This lets you describe all mutually exclusive navigation paths in a single state (enum), then present them differently. Or even decide the presentation based on something inside that state.

The App

The app navigates through mathematical operators to modify a number. In so you can navigate infitely forward and backward. A 'counter' screen lets you capture a new number and exercises cancellable effects.

Or, "fork" the current state into a sheet and navigate independently of the initial number. Again, recursively forever.

Highlights

Pulling that all together, the interesting parts are:

Modeling all possible navigation paths in an enum

indirect enum ScreenState: Equatable {
    case counter(CounterState)
    case operators(OperatorsState)
}

Rendering all of those screens with a SwitchStore:

struct ScreenSwitchStore: View {
    let store: Store<ScreenState, ScreenAction>
    var body: some View {
        SwitchStore(store) {
            CaseLet(
                state: /ScreenState.counter,
                action: ScreenAction.counter,
                then: CounterView.init
            )
            CaseLet(
                state: /ScreenState.operators,
                action: ScreenAction.operators,
                then: OperatorsView.init
            )
        }
    }
}

Rendering all possible presentations of those screens:

struct PresentedScreenSwitchStore: View {
    let type: PresentationType
    let store: Store<ScreenState, ScreenAction>
    var body: some View {
        switch type {
        case .navigation:
            ScreenSwitchStore(store: store)
        case .sheet, .fullScreenCover:
            NavigationView {
                ScreenSwitchStore(store: store)
            }
            .navigationViewStyle(StackNavigationViewStyle())
        }
    }
}

Presenting views at runtime:

var body: some View {
    List {
        // content
    }
    .presentation(
        type: { state in
            switch state {
            case .operators(let op):
                return op.isFork ? .sheet : .detailNavigation
            case .counter:
                return .detailNavigation
            }
        },
        store: store,
        state: \.presentation,
        action: OperatorsAction.presentation,
        dismiss: OperatorsAction.dismiss,
        destination: PresentedScreenSwitchStore.init
    )
}

License

MIT