샘성의 iOS 개발 일지

ReactorKit을 MVVM에 넣어보기 본문

iOS/ReactiveX

ReactorKit을 MVVM에 넣어보기

SamusesApple 2023. 7. 2. 21:21
728x90

1. ReactorKit이란? 

   단방향 데이터 흐름을 가진 반응형 앱에 적합한 프레임워크.
기본적으로 RxSwift를 활용하고 있고 개인적으로 더 아키텍처가 통일된 MVVM(?)이라는 느낌을 준다.

 

2. GitHub : 

 

GitHub - ReactorKit/ReactorKit: A library for reactive and unidirectional Swift applications

A library for reactive and unidirectional Swift applications - GitHub - ReactorKit/ReactorKit: A library for reactive and unidirectional Swift applications

github.com

 

 

 

3. ReactorKit의 구조

 

출처 : https://github.com/ReactorKit/ReactorKit

     1. View

      말 그대로 View. UI를 담당하고 사용자와 상호작용의 역할을 수행.
    Reactor의 State(상태)를 구독하고 있으며, State에 따라 UI를 변경함.

    2. Reactor

      View로부터 일어나는 액션을 받아 처리하는 역할을 함 (마치 ViewModel)
    View의 Action을 미리 정의하고, view로부터 받은 action을 처리 후 다시 View에게 상태(State)를 전달함.

    3. State :

      View의 액션이 일어나면, 변경될 수 있는 상태를 구조체 형태로 나타낸 것.
    View는 이 State를 구독하고 State에 반응하여 UI를 변경한다.

    4. Action :

      View에서 사용자와 상호작용에서 일어나는 액션을 Enum 타입으로 정의한 것. (Input)

 

 

 

4. ReactorKit의 흐름

  1. View에서 액션이 일어남
  2. View에서 일어난 액션을 Reactor가 미리 정의해놓은 액션으로 받음
  3. mutate()에 해당 Action에 대해 어떤 작업(Mutation)을 처리할지 전달
  4. reduce()에서 작업 수행 + 수행된 작업에 대한 결과(State)를 View에게 전달
  5. State를 구독하고 있던 View의 UI가 업데이트 됨

 

 

5. ReactorKit을 적용한 MVVM 코드 (버튼으로 숫자 + - 하기)

* ReactorKit을 적용한 ViewModel


import Foundation
import ReactorKit

final class TransactionViewModel: ViewModel, Reactor {
    
    let initialState: State
    
    var account: BankAccount
    
    private var disposeBag = DisposeBag()
    
    // Input
    /// view로부터 받는 action 정의
    enum Action {
        case deposit(Int)
        case withdraw(Int)
    }
    
    /// Action에 대한 작업 단위 정의
    enum Mutation {
        case increaseBalance(Int)
        case decreaseBalance(Int)
    }
    
    // Output
    /// 현재 상태, view는 State를 구독하여 UI를 업데이트 함
    struct State {
        let currentBalance: Int
    }
    
    // MARK: - Initializer
    
    init(viewModel: ViewModel) {
        self.account = viewModel.account
        self.initialState = State(currentBalance: viewModel.account.balance)
    }
    
    // MARK: - Transform
    
    func mutate(action: Action) -> Observable<Mutation> {
        switch action {
        case .deposit(let amount):
            print("+\(amount)")
            return Observable.create { emitter in
                emitter.onNext(Mutation.increaseBalance(amount))
                return Disposables.create()
            }
        case .withdraw(let amount):
            print("-\(amount)")
            return Observable.create { emitter in
                emitter.onNext(Mutation.decreaseBalance(amount))
                return Disposables.create()
            }
        }
    }
    
    func reduce(state: State, mutation: Mutation) -> State {
        switch mutation {
        case .increaseBalance(let int):
            return State(currentBalance: state.currentBalance + int)
        case .decreaseBalance(let int):
            return State(currentBalance: state.currentBalance - int)
        }
    }
}

 

* ViewController

import UIKit
import SnapKit
import Then
import RxCocoa
import ReactorKit

final class TransactionViewController: UIViewController, View {
    
    private var reactor: TransactionViewModel
    
    var disposeBag: RxSwift.DisposeBag
    
    // MARK: - Components
    
    private let balanceView = BalanceLabelView()
    
    private let amonutTextField = UITextField().then {
        $0.borderStyle = .roundedRect
        $0.layer.borderWidth = 0.5
        $0.layer.borderColor = UIColor.gray.cgColor
        $0.layer.cornerRadius = 8
        $0.textAlignment = .center
        $0.keyboardType = .numberPad
        $0.font = UIFont.systemFont(ofSize: 22, weight: .medium)
        $0.placeholder = "Type price"
    }
    
    private let depositButton = TransactionButton(action: .deposit(0))
    
    private let withdrawButton = TransactionButton(action: .withdraw(0))
    
    // MARK: - Lifecycle
    
    override func loadView() {
        self.view = balanceView
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        
        setAutolayout()

        bind(reactor: reactor)
    }
        
    init(viewModel: ViewModel) {
        self.reactor = TransactionViewModel(viewModel: viewModel)
        self.disposeBag = DisposeBag()
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: - Bind

    func bind(reactor: TransactionViewModel) {
        bindButtonAction(reactor)
        bindState(reactor)
    }
    
    // 버튼에 대한 액션 reactor에 전달
    private func bindButtonAction(_ reactor: TransactionViewModel) {
        depositButton.rx.tap
            .filter({ [weak self] in
                self!.amonutTextField.text!.count > 0
            })
            .map({ [weak self] in
                self!.amonutTextField.text!
            })
            .map({ Reactor.Action.deposit(Int($0)!) })
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
        
        withdrawButton.rx.tap
            .filter({ [weak self] in
                self!.amonutTextField.text!.count > 0
            })
            .map({ [weak self] in
                self!.amonutTextField.text!
            })
            .map({ Reactor.Action.withdraw(Int($0)!) })
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
    }
    
    // UI 업데이트
    private func bindState(_ reactor: TransactionViewModel) {
        reactor.state
            .map({ $0.currentBalance })
            .map({ "\($0)"})
            .bind(to: balanceView.balanceLabel.rx.text)
            .disposed(by: disposeBag)
    }
    ...
}

 

 

6. 결과

728x90