Swift Composable Architecture - Reduce to action from environment change

Hi,

thanks for TCA I was struggling with some SwiftUI related things, but TCA make it more easier ! I just have question about reducer.

I want to use SwitchLet to define if is user logged in or logged out. I'm observing some UseCase which returns PassthrougSubject<User?, Never>. But I don't know how to make it properly and I'm getting Member 'loggedOut' expects argument of type 'RootLoggedOutAction'.

What I tried:

Reducer { state, action, env in
        env.observeUserChanges.invoke()
            .flatMap { user -> AnyPublisher<AppAction, Never> in
                guard let user = user else {
                    return Just(.loggedOut).eraseToAnyPublisher()
                }
                return Just(.loggedIn).eraseToAnyPublisher()
            }
            .eraseToEffect()
    }

Where AppState:

enum AppState: Equatable {
    case loggedIn(RootLoggedInState)
    case loggedOut(RootLoggedOutState)
    
    public init() { self = .loggedOut(.init()) }
}

AppAction:

enum AppAction: Equatable {
    case loggedIn(RootLoggedInAction)
    case loggedOut(RootLoggedOutAction)
}

Full appReducer:

let appReducer = Reducer<AppState, AppAction, AppEnvironment>.combine(
    rootLoggedInReducer.pullback(
        state: /AppState.loggedIn,
        action: /AppAction.loggedIn,
        environment: { env in env }
    ),
    rootLoggedOutReducer.pullback(
        state: /AppState.loggedOut,
        action: /AppAction.loggedOut,
        environment: { env in env }
    ),
    Reducer { state, action, env in
        env.observeUserChanges.invoke()
            .flatMap { user -> AnyPublisher<AppAction, Never> in
                guard let user = user else {
                    return Just(.loggedOut).eraseToAnyPublisher()
                }
                return Just(.loggedIn).eraseToAnyPublisher()
            }
            .eraseToEffect()
    }
)

Can I reduce it to Effect just from environment, or do I need to do it in some other way?

I'm sorry, if it is smaller Swift problem, but I am Android developer and I am trying to learn Swift :D

Thanks,
Vlado

Check the types of both Just publishers. Output of these is not AppAction.

let publisher1 = Just(.loggedOut)  // Just<(RootLoogedOutAction) -> AppAction>
let publisher2 = Just(.loggedIn)  // Just<(RootLoogedInAction) -> AppAction>

You are not providing associated values for AppAction enum cases, so they are function from associated value to enum value.

However, I guess that you really don't want to start new long running observing effect on every action sent to appReducer. Additionally, I would recommend to always send side effect with explicit action or in with higher order reducer.

Personally I would define new AppActions to handle monitoring user status and updating state.

enum AppAction: Equatable {
  case loggedIn(RootLoggedInAction)
  case loggedOut(RootLoggedOutAction)
  case startObservingUserStatus
  case updateState(AppState)
}

let appReducer = Reducer<AppState, AppAction, AppEnvironment>.combine(
    rootLoggedInReducer.pullback(
        state: /AppState.loggedIn,
        action: /AppAction.loggedIn,
        environment: { env in env }
    ),
    rootLoggedOutReducer.pullback(
        state: /AppState.loggedOut,
        action: /AppAction.loggedOut,
        environment: { env in env }
    ),
    
  .init { state, action, env in
    switch action {
      case .startObservingUserStatus:
        return env.userStatusObserver()   // Side effect which monitors user status and publish relevant state
          .map(AppAction.updateState)  // maps state to AppAction
      case let .updateState(newState):
        state = newState  // updates state
        return .none
      case .loggedIn:
        return .none
      case .loggedIn:
        return .none
    }
)

You should probably additionally handle cancelling this observing effect. You can send .startObservingUserStatus in appDidFinishLunching function or in .onAppear / .task view modifier.

Important disclaimer: I'm also beginner, so please forgive me if I made some mistake or I misunderstood your problem.