Skip to content

bocato/tca-boundaries

Repository files navigation

TCABoundaries

The TCAFeatureAction defines a pattern for actions on TCA based on https://www.merowing.info/boundries-in-tca/ Its idea is to set a well defined specification for actions on TCA views, where ideally View and Internal actions should not go out of the feature scope.

Example:

enum ExampleAction: TCAFeatureAction {
    enum ViewAction: Equatable {
        case onTapLoginButton
    }
     
    enum DelegateAction: Equatable {
        case notifyLoginSuccess
    }

    enum InternalAction: Equatable {
        case loginResult(Result<String, NSError>)
    }
    
    case view(ViewAction)
    case delegate(DelegateAction)
    case _internal(InternalAction)
}

Example app

Below is a small counter feature that demonstrates how the boundaries pattern works together with the latest Composable Architecture APIs.

import ComposableArchitecture
import TCABoundaries
import SwiftUI

@Reducer
struct CounterFeature: BoundingReducer {
    struct State: Equatable {
        var count = 0
    }

    enum Action: TCAFeatureAction {
        enum ViewAction: Equatable {
            case decrementButtonTapped
            case incrementButtonTapped
        }

        enum DelegateAction: Equatable {
            case finished
        }

        enum InternalAction: Equatable {
            case timerUpdated
        }

        case view(ViewAction)
        case delegate(DelegateAction)
        case _internal(InternalAction)
    }

    func reduce(into state: inout State, viewAction action: Action.ViewAction) -> Effect<Action> {
        switch action {
        case .decrementButtonTapped:
            state.count -= 1
            return .none

        case .incrementButtonTapped:
            state.count += 1
            return .none
        }
    }
}

@ViewAction(for: CounterFeature.self)
struct CounterView: View {
    let store: StoreOf<CounterFeature>

    var body: some View {
        WithPerceptionTracking {
            VStack {
                Text("\(store.count)")

                HStack {
                    Button("") { send(.decrementButtonTapped) }
                    Button("+") { send(.incrementButtonTapped) }
                }
            }
        }
    }
}

Examples

Child flow on parent, with boundaries

When you have a Child flow inside a Parent store and need to scope it is often expressed as an InternalAction of the Parent store.

enum ParentAction: TCAFeatureAction {
    enum InternalAction: Equatable {
    // ...
        case child(ChildAction)
    }
    // ...
    case view(ViewAction)
    case _internal(InternalAction)
    case delegate(DelegateAction)
}

This will be expressed like below:

// On the reducer 
Scope(state: \.child, action: /Action.InternalAction.child) {
    ChildFeature()
}
// On the view
struct ParentView: View {
    let store: StoreOf<ParentReducer>
    
    var body: some View {
        // ...
        ChildView(
            store.scope(
                state: \.childState,
                action: /ParentAction.InternalAction.child
             )
         )
         // ...
     }
}

Bounding Reducers

Bounding reducers is a protocol to define a standard when implementing reducers with Boundaries. We can have them on composed (reducers with body) or non-composed reducers.

The main idea relies on setting a standard for separating the actions based on its type on specific functions:

// To handle actions coming from the view
func reduce(into state: inout State, viewAction action: Action.ViewAction) -> Effect<Action>

// To handle actions that happen inside the reducer
func reduce(into state: inout State, internalAction action: Action.InternalAction) -> Effect<Action>

// To handle actions that where delegated to this reducer
func reduce(into state: inout State, delegateAction action: Action.DelegateAction) -> Effect<Action>

Example for non-composed reducers:

struct SomeFeature: BoundingReducer {
      func reduce(into state: inout State, viewAction action: Action.ViewAction) -> Effect<Action> {
        switch action {
         ...
        }
     }

    func reduce(into state: inout State, internalAction action: Action.InternalAction) -> Effect<Action> {
        switch action {
         ...
        }
    }

    ...
}

Example for composed reducers:

struct SomeFeature: ComposedBoundingReducer {
      var body: some Reducer<State, Action> {
        coreReducer
            .ifLet(\.child, action: /Action.InternalAction.child) {
                ChildFeature()
            }
      }

      func reduce(into state: inout State, viewAction action: Action.ViewAction) -> Effect<Action> {
        switch action {
         ...
        }
     }
    ...
}

Installation

You can add TCABoundaries to an Xcode project by adding it as a package dependency.

  1. From the File menu, select Add Packages...
  2. Enter "https://github.com/bocato/tca-boundaries" into the package repository URL text field

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages