CombineProcessor

Combine, State machine, Testable

Woody Liu
4 min readApr 4, 2023

As an avid user of the TCA architecture, I often find myself unable to use it in real-world work situations due to various factors, such as team goals, architecture, and versioning. However, for core and laborious functionalities that require state machines, I rely on CombineProcessor.

CombineProcessor is a lightweight state machine that takes inspiration from the TCA Reducer design. It removes UI layer APIs and introduces simple Combine extensions and meticulously designed interfaces for Unit Testing. This allows developers to concentrate on the interaction between states, while Processor handles testing.

Overall, CombineProcessor is a practical tool for state machine design that enables developers to focus on state interactions, with two simple ways of verifying state transitions' correctness. This makes state machine design a lot easier and more convenient.

Introduce

CombineProcessor is a very useful and convenient tool that allows the design of state machines to focus more on the interactions between states. It also provides two ways to verify if the order of state transitions is correct.

  • Console Output:
    By filtering Processor ID: in the Xcode Console, you can see a complete printout of the handling time for each state and their interactions. This is very useful for debugging and understanding program execution.
// 036 為 Processor ID 末三碼
Processor ID: 036 - - Action - emailSignIn - date: 2023-04-04 05:48:17 +0000
Processor ID: 036 - - PrivateAction - signInEmail: dadwad, password: dadwad - date: 2023-04-04 05:48:17 +0000
Processor ID: 036 - - PrivateAction - fetchSomeToken dadwad - date: 2023-04-04 05:48:19 +0000
Processor ID: 036 - - PrivateAction - storeUser - date: 2023-04-04 05:48:20 +0000
Processor ID: 036 - - PrivateAction - updateStauts - date: 2023-04-04 05:48:20 +0000
  • Unit Test:
    Users can use the test interface to verify the order of state transitions. These test interfaces can simulate different state transitions and verify if the processing of each state meets expectations. This way, users can be more confident in the correctness of the state machine.

await processor.test.composable.privateAction(sendAction: .appleSignIn,
equal: PrivateAction.A)
.nextPrivateAction(equal: PrivateAction.B)
.nextPrivateAction(equal: PrivateAction.C)
.nextPrivateAction(equal: PrivateAction.C)
.nextPrivateAction(equal: PrivateAction.E)

CombineProcessor provides a very convenient tool for the design of state machines, allowing users to focus more on the interactions between states. It also provides two ways to verify if the order of state transitions is correct.

SignInProcessor

Example for CombineProcessor

Suppose we have a login requirement where users can choose to log in with either an `email` or `Apple` account. After completing the login process, `Firebase Authorization` is used to verify the user and obtain their information, followed by obtaining an authentication token from our `own server` to complete the login flow.

So we have three steps:

  • Email Sign In or Apple Sing In
  • Firebase Authorization
  • Own-Server Authorization

First

Define Components

  • Action:
    An event that occurs in your application that can cause a state change.
  • PrivateAction:
    An action that can only be dispatched internally by the processor.
  • State:
    The current state of your application, stored as a struct.
  • Environment(Optional):
    A container for all the methods that UseCases or state machines need to use.

Please refer to the link for complete information.

 enum Action {

case appleSignIn

case emailSignIn(_ email: String, _ password: String)

case logout

var privateAction: PrivateAction {
switch self {
case .appleSignIn: return .appleSignIn
case .emailSignIn(let emil, let password): return .signInEmail(with: emil, password: password)
case .logout: return .ready
}
}
}

enum PrivateAction {

case appleSignIn
case signInEmail(with: String,
password: String)

case signInProviderID(appleUser: AppleUser)

...
}

struct Environment { ... }

struct States: Equatable { ... }

Second

Assemble State, Action, PrivateAction, and Environment using AnyProcessorReducer.

var reducer: AnyProcessorReducer<States, Action, PrivateAction, Environment> {
return AnyProcessorReducer(mutated: { action in
return action.privateAction
}, reduce: { states, privateAction, environment -> ProcessorPublisher<PrivateAction, Never>? in

switch privateAction {

case .appleSignIn:
states.stauts = .isSignIning
return environment
.appleService.tryStart()
.catchToDeresultProcessor(onSuccess: PrivateAction.storeAppleUser,
onError: PrivateAction.receiveError)

case .signInEmail(with: let email, password: let password):
states.stauts = .isSignIning
return environment
.firebaseService
.signIn(with: email, password: password)
.catchToDeresultProcessor(onSuccess: { response in
return .fetchSomeToken(userFactory:
.init(firebaseResponse: response),
email: response.email ?? "Null",
idToken: response.idToken)
}, onError: PrivateAction.receiveError)
...
}

Final

Injection in Processor


let processor = Processor(
initialState: States(stauts: .ready),
reducer: reducer,
environment: Environment()
)

Completed Test

await processor.test.composable.privateAction(sendAction: .appleSignIn, where: { action in
guard let action else { return false }
if case SignInProcessor.PrivateAction.appleSignIn = action {
return true
}
return false
})
.nextPrivateAction(where: { action in
guard let action else { return false }
if case SignInProcessor.PrivateAction.storeAppleUser = action {
return true
}
return false
}).nextPrivateAction(where: {action in
guard let action else { return false }
if case SignInProcessor.PrivateAction.signInProviderID = action {
return true
}
return false
})

Congratulations

You have completed the design of your login system and can now move on to designing other parts of your app, such as ViewModel and UI.

--

--