본문 바로가기
Programming Paradigm/ReactorKit

ReactorKit으로 단방향 반응형 앱 만들기 - 전수열

by 탄이. 2020. 3. 4.

ReactorKit으로 단방향 반응형 앱 만들기 - 전수열

ReactorKit으로 단방향 반응형 앱 만들기

  1. Massive View Controller를 피하기 위해

    Why?

    • 뷰와 로직의 관심사 분리
    • 뷰 컨트롤러가 단순해짐
  2. RxSwift의 장점을 모두 취한다.

    • ReactorKit은 RxSwift를 기반으로 함
    • 모든 RxSwift 기능을 사용 가능
  3. 상태 관리가 쉽다.

    • 단방향 데이터 흐름
    • 중간 상태를 reduce() 함수(pure function)로 관리
      • pure function(순수 함수)
    • 상태 관리가 간결해짐

View

  • 사용자 입력을 받아서 Reactor에 전달
  • Reactor로부터 받은 상태를 렌더링
  • 뷰 컨트롤러, 셀, 컨트롤 등을 모두 View로 취급

Reactor

  • View에서 전달받은 Action에 따라 로직 수행
  • 상태를 관리하고 상태가 변경되면 View에 전달
  • 대부분의 View는 대응되는 Reactor를 가짐

Mutation

  • Action이 State를 바로 변경하지는 않음
  • 비동기 타임에 State가 변경되는 경우가 있음
  • Action과 State 사이에 Mutation을 둬서 비동기 처리
  • Mutation은 State를 변경하는 명령/작업 단위
  • Mutation은 View에 노출되지 않음

mutate() - 변이

func mutate(Action) -> Observable<Mutation> {}

reduce() - 환원

func reduce(State, Mutation) -> State {}

Reactor

import ReactorKit
import RxSwift

final class CounterViewReactor: Reactor {
    enum Action {
        case increase
        case decrease
    }

    enum Mutation {
        case increaseValue
        case decreaseValue
                case setLoading(Bool)
    }

    struct State {
        var value: Int = 0
                var isLoading: Bool = false
    }

    let initialState: State = State()

    func mutate(action: Action) -> Observable<Mutation> {
        switch action {
        case .increase:
            return Observable.concat([
                Observable.just(Mutation.setLoading(true)),
                Observable.just(Mutation.increaseValue)
                .delay(1, scheduler: MainScheduler.instance),
                Observable.just(Mutation.setLoading(false))
            ])

        case .decrease:
            return Observable.concat([
                Observable.just(Mutation.setLoading(true)),
                Observable.just(Mutation.decreaseValue)
                    .delay(1, scheduler: MainScheduler.instance),
                Observable.just(Mutation.setLoading(false))
                ])
        }
    }

    func reduce(state: State, mutation: Mutation) -> State {
        var newState = state

        switch mutation {
        case .increaseValue:
            newState.value += 1
        case .decreaseValue:
            newState.value -= 1
        case .setLoading(isLoading):
            newState.isLoading = isLoading
        }
        return newState
    }
}

ViewController

import ReactorKit
import RxSwift
import RxCocoa

final class CounterViewController: UIViewController, StoryboardView {

    @IBOutlet var increaseButton: UIButton!
    @IBOutlet var decreaseButton: UIButton!
    @IBOutlet var valueLabel: UILabel!
    @IBOutlet var activityIndicatorView: UIActivityIndicatorView!

    var disposeBag = DisposeBag()

    func bind(reactor: CounterViewReactor) {
        // Action
        increaseButton.rx.tap
            .map { Reactor.Action.increase }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)

        decreaseButton.rx.tap
            .map { Reactor.Action.decrease }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)

        // State
        reactor.state.map { $0.value }
            .distinctUntilChanged()
            .map { "\($0)" }
            .bind(to: valueLabel.rx.text)
            .disposed(by: disposeBag)

        reactor.state.map { $0.isLoading }
            .distinctUntilchanged()
            .bind(to: activityIndicatorView.rx.isAnimating)
            .disposed(by: disposeBag)
    }
}

AppDelegate

...
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

    let counterVC = window?.rootViewController as? CounterViewController
    let counterViewReactor = CounterViewReactor()
    counterVC?.reactor = counterViewReactor

    return true
}
...

댓글