Liam SY Kim
by Liam SY Kim
4 min read

Categories

I often have heard that ReactorKit usefully give structure of RxSwift.

Because I am junior developer, I am not sure that I can learn ReactorKit at first. However, I realized that Reactor pattern is similar with many other patterns. I heard that it is same pattern with Redux, and additionally I can know it is also similar with Bloc Pattern in Flutter as well.

Because I used to use flutter bloc in flutter, It is possible to learn at once for me.

Concept

ref : github.com/ReactorKit/ReactorKit

In this image, Reactor’s role is like state manager. As you guys know, When the action comes asynchoronously from view, There exists delay to complete the action and to make a state.

Mutate() processes action, converts to mutation, and sends the mutation to reduce() function. Because reduce() returns state which is converted from mutation, we can reflect state from the reactor immedietly on view.

Furthermore, we usually need to bind servies api on reactor to show view state. In this case, mutate() sends state to the view based on action, and service. Mutate() also can send state to service.

From the reactor, Action is input, and state is output. these action (input) and state (output) is stream, and we can implement it using RxSwift.

Next, Here are one-line explaination for each reactor functions from ReactorKit

  • mutate() : receives an Action and generates an Observable.
  • reduce() : generates a new State from a previous State and a Mutation.
  • transform() : transforms each stream. There are three transform() functions:

My examples are modified version of ReactorKit counter example, and I will explain the example which I modified above.

Let’s dive into the example.

Fruit Example

It is smiple example which can show fruit image when the user clicks button of fruits.

When the user clicks the orange, then reactor immediatly changes the state as ‘loading’ during downloading image(In this example, it just give 500 miliseconds time delay).

If app finished to download image, then state changed to ‘orange’, and view will show the orange image.

This example has two classes

  • FruitViewController
  • FruitViewReactor

FruitViewController

Declared Variable

As I mentioned above, there are five fruit buttons.

Additionally, we binds FruitViewReactor on ViewController.

class FruitViewController: UIViewController {

    @IBOutlet weak var appleButton: UIButton!
    @IBOutlet weak var grapesButton: UIButton!
    @IBOutlet weak var bananaButton: UIButton!
    @IBOutlet weak var cherriesButton: UIButton!
    @IBOutlet weak var orangeButton: UIButton!
    
    @IBOutlet weak var fruitImage: UIImageView!
    var disposeBag = DisposeBag()
    
    let fruitReactor = FruitViewReactor()
    ...
}

Bind Reactor

Here are the function which is binded with FruitViewReactor.

Each button IB is tapped by RxSwift, and sends transformed event which is ‘reactor action’ to the binded RruitViewReactor.

class FruitViewController: UIViewController {

   ...
    override func viewDidLoad() {
        super.viewDidLoad()
        
        bind(reactor: fruitReactor)
    }
    
    func bind(reactor: FruitViewReactor) {
        
        appleButton.rx.tap
            .map{ FruitViewReactor.Action.apple }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
        
        grapesButton.rx.tap
            .map{ FruitViewReactor.Action.grapes }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
        
        bananaButton.rx.tap
            .map{ FruitViewReactor.Action.banana }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
        
        cherriesButton.rx.tap
            .map{ FruitViewReactor.Action.cherries }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
        
        orangeButton.rx.tap
            .map{ FruitViewReactor.Action.orange }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
        
        ...
}

Above one is reactor’s input, this one is output of the reactor.

State of reactor sends to viewcontroller. After subscribing state from the viewcontroller, viewcontroller would set the fruit image as the state value. Because the state value is same as image name, the fruit image can be shown by UIImageView on viewcontroller.

class FruitViewController: UIViewController {

    ...
    
    func bind(reactor: FruitViewReactor) {
        
       ...
        reactor.state.map { $0.fruitName }
          .distinctUntilChanged()
          .map { "\($0)" }
            .subscribe(onNext: { val in
                self.fruitImage.image = UIImage(named: val)
            })
          .disposed(by: disposeBag)
        
        reactor.state.map { $0.isLoading }
        .distinctUntilChanged()
          .subscribe(onNext: { val in
            if val == true{
                self.fruitImage.image = UIImage(named: "loading")
            }
          })
        .disposed(by: disposeBag)

    }
}

Fruit Reactor

Until now, We saw the input and output streams from the view. Next, we dive into the reactor’s internal side which converts action(event) to state.

Declared Variable

we should declare action and mutation by enum.

In this example, Reactor gets action as kind of fruits, and converts to mutation which is asking the fruit image.

State is declared as struct which is initialized by the mutation.

final class FruitViewReactor: Reactor {

    // Action
    enum Action {
        case orange
        case apple
        case cherries
        case banana
        case grapes
    }
    
   
    enum Mutation {
        case orangeImage
        case appleImage
        case cherriesImage
        case bananaImage
        case grapesImage
        case setLoading(Bool)
    }
    
   // State
    struct State {
        var fruitName: String
        var isLoading: Bool
    }
    
    let initialState: State
    
    init() {
        self.initialState = State(
            fruitName: "",
            isLoading: false
        )
    }
    
   ...
}

Action -> Mutation

As I mentioned above, I give 500 miliseconds delay during occuring mutation of fruit image.

Concat operater in RxSwift sends serialized events, and those events are adding as group after previous event observed.

Because it is among ‘setLoading’ mutation, mutate() can send loading state to the view during the delay.

final class FruitViewReactor: Reactor {
    ...
    
    // Action -> Mutation
    func mutate(action: Action) -> Observable<Mutation> {
        switch action {
        case .orange:
            return Observable.concat([
                Observable.just(Mutation.setLoading(true)),
                Observable.just(Mutation.orangeImage).delay(.milliseconds(500), scheduler: MainScheduler.instance),
                Observable.just(Mutation.setLoading(false)),
                
            ])
        case .apple:
            return Observable.concat([
                Observable.just(Mutation.setLoading(true)),
                Observable.just(Mutation.appleImage).delay(.milliseconds(500), scheduler: MainScheduler.instance),
                Observable.just(Mutation.setLoading(false)),
            ])
        case .cherries:
            return Observable.concat([
                Observable.just(Mutation.setLoading(true)),
            Observable.just(Mutation.cherriesImage).delay(.milliseconds(500), scheduler: MainScheduler.instance),
                Observable.just(Mutation.setLoading(false)),
            ])
        case .banana:
            return Observable.concat([
                Observable.just(Mutation.setLoading(true)),
                Observable.just(Mutation.bananaImage).delay(.milliseconds(500), scheduler: MainScheduler.instance),
                Observable.just(Mutation.setLoading(false)),
            ])
        case .grapes:
            return Observable.concat([
                Observable.just(Mutation.setLoading(true)),
                Observable.just(Mutation.grapesImage).delay(.milliseconds(500), scheduler: MainScheduler.instance),
                Observable.just(Mutation.setLoading(false)),
            ])
        }
    }
    
    ...
}

Mutation -> State

reduce() returns the state of which image name in assets.

this returned value is given to viewcontroller UIimageview.image. Finally, viewcontroller will show the image because of this return value.

final class FruitViewReactor: Reactor {
    ...
    
    // Mutation -> State
    func reduce(state: State, mutation: Mutation) -> State {
        var state = state
        switch mutation {
            
        case .orangeImage:
            state.fruitName = "orange"
        case .appleImage:
            state.fruitName = "apple"
        case .cherriesImage:
            state.fruitName = "cherries"
        case .bananaImage:
            state.fruitName = "banana"
        case .grapesImage:
            state.fruitName = "grapes"
        case .setLoading(let val):
            state.isLoading = val
        }
        return state
    }
}

Conclusion

Because I also studying with example, I can make mistake or omit some details. Feel free to comment me what you want to modify from this example and explaination.