apple/RxSwift, ReactorKit

[ReactorKit] ReactorKit 공부하기 #1

lgvv 2022. 7. 24. 02:39

ReactorKit 공부하기 #1

 

해당 포스팅은 ReactorKit 3.2.0을 기준으로 함

 

오늘은 ReactorKit에 대해서 공부해보려고 함.

MVVM을 사용하고 있는데 무신사 등에서 리액터 킷을 사용하고 있대서 학습해보고자 함.

 

오픈소스 링크 및 설명

 

Basic Concept

ReactorKit은 반응적이고 단방햑적인 Swift 아키텍처를 위한 프레임워크

 

ReactorKit 설명

 

 

ReactorKit은 Flux와 Reactive Programming의 조합

  • user의 Action과 View의 상태는 observable streams을 통해 각각의 레이어에 전달
  • 이러한 스트림은 단방향 스트림이고, 뷰는 오로지 emit action만 방출할 수 있고 reactor는 state만 방출할 수 있.

 

 

위 형태로 사이클 구선

 

 

코드 예시

Reactor를 정의하기

//
//  CounterViewReactor.swift
//  ReactorKitPractice
//
//  Created by Hamlit Jason on 2022/07/23.
//

import Foundation
import RxSwift
import RxCocoa
import ReactorKit

class CounterViewReactor: Reactor {
    /// 초기 상태를 정의합니다.
    let initialState = State()
    
    /// 사용자 행동을 정의합니다.
    ///
    /// 사용자에게 받을 액션
    enum Action {
        case increase
        case decrease
    }
    
    /// 처리 단위를 정의합니다.
    ///
    /// 액션을 받았을 때 변화
    enum Mutation {
        case increaseValue
        case decreaseValue
        case setLoading(Bool)
    }
    
    /// 현재 상태를 기록합니다.
    ///
    /// 어떠한 변화를 받은 상태!
    struct State {
        var value = 0
        var isLoading = false
    }
    
    /// Action이 들어온 경우 어떤 처리를 할 것인지 분기
    ///
    /// Mutation에서 정의한 작업 단위들을 사용하여 Observable로 방출
    ///
    /// 액션에 맞게 행동해!
    func mutate(action: Action) -> Observable<Mutation> {
        switch action {
        case .increase:
            return Observable.concat([ // concat은 평등하게 먼저 들어온 옵저버블을 순서대로 방출
                Observable.just(.setLoading(true)),
                Observable.just(.increaseValue).delay(.seconds(1), scheduler: MainScheduler.instance),
                Observable.just(.setLoading(false))
            ])
        case .decrease:
            return Observable.concat([
                Observable.just(.setLoading(true)),
                Observable.just(.decreaseValue).delay(.seconds(1), scheduler: MainScheduler.instance),
                Observable.just(.setLoading(false))
            ])
        }
    }
    
    /// 이전 상태와 처리 단위를 받아서 다음 상태를 반환하는 함수
    ///
    /// mutate(action: )이 실행되고 난 후 바로 해당 메소드를 실행
    ///
    /// 변화에 맞게끔 값을 설정해!
    func reduce(state: State, mutation: Mutation) -> State {
        var newState = state
        switch mutation {
        case .increaseValue:
            newState.value += 1
        case .decreaseValue:
            newState.value -= 1
        case .setLoading(let isLoading):
            newState.isLoading = isLoading
        }
        return newState
    }
}

 

 

CounterViewController를 정의하고 리액터 연결하기

 

//
//  CounterViewController.swift
//  ReactorKitPractice
//
//  Created by Hamlit Jason on 2022/07/23.
//

import UIKit
import RxSwift
import RxCocoa
import ReactorKit
import SnapKit

class CounterViewController: UIViewController, View {
    
    var disposeBag = DisposeBag()
    let counterViewReactor = CounterViewReactor()
    
    lazy var increaseButton = UIButton()
    lazy var countLabel = UILabel()
    lazy var decreaseButton = UIButton()
    lazy var loadingIndicator = UIActivityIndicatorView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        setupView()
        bind(reactor: counterViewReactor)
    }
    
    func bind(reactor: CounterViewReactor) {
        bindAction(reactor: reactor)
        bindState(reactor: reactor)
    }
    
    private func bindAction(reactor: CounterViewReactor) {
        increaseButton.rx.tap
            .map { CounterViewReactor.Action.increase }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
        
        decreaseButton.rx.tap
            .map { CounterViewReactor.Action.decrease }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
    }
    
    private func bindState(reactor: CounterViewReactor) {
        reactor.state
            .map { "\($0.value)" }
            .distinctUntilChanged()
            .bind(to: countLabel.rx.text)
            .disposed(by: disposeBag)
        
        reactor.state
            .map { $0.isLoading }
            .distinctUntilChanged()
            .bind(to: loadingIndicator.rx.isAnimating)
            .disposed(by: disposeBag)
        
        reactor.state
            .map { !$0.isLoading }
            .distinctUntilChanged()
            .bind(to: loadingIndicator.rx.isHidden)
            .disposed(by: disposeBag)
    }
}

extension CounterViewController {
    var margin: CGFloat {
        get { return 10.0 }
    }
    
    func setupView() {
        view.addSubview(increaseButton)
        increaseButton.setImage(UIImage(systemName: "plus"), for: .normal)
        
        increaseButton.snp.makeConstraints {
            $0.width.height.equalTo(30)
            $0.centerY.equalToSuperview()
            $0.leading.equalTo(margin)
        }
        
        view.addSubview(countLabel)
        countLabel.text = "0"
        
        countLabel.snp.makeConstraints {
            $0.center.equalToSuperview()
        }
        
        view.addSubview(decreaseButton)
        decreaseButton.setImage(UIImage(systemName: "minus"), for: .normal)
        
        decreaseButton.snp.makeConstraints {
            $0.width.height.equalTo(30)
            $0.centerY.equalToSuperview()
            $0.trailing.equalTo(-margin)
        }
        
        view.addSubview(loadingIndicator)
        
        loadingIndicator.snp.makeConstraints {
            $0.centerX.equalToSuperview()
            $0.centerY.equalToSuperview().offset(100)
        }
    }
}