iosswift 2022. 10. 12. 01:03

ReactorKit 이란 ? 

단방향 데이터 흐름을 가진 반응형 앱을 위한 Framework

View: 

사용자 입력을 받아서 Reactor 에 전달 (Action)

Reactor로부터 받은 상태를 Rendering

ViewController, Cell, Control 등을 모두 View 로 취급

 

protocol View {
    associatedtype Reactor
    
    var disposeBag: DisposeBag
    
    // self.reactor가 바뀌면 호출됨.
    func bind(reactor: Reactor)
}

 

 

Reactor

View 에서 전달받은 Action 에 따라 로직 수행

상태를 관리하고 상태가 변경되면 View 에 전달

대부분의 View 는 대응되는 Reactor 를 가짐

protocol Reactor {
    associatedtype Action
    associatedtype Mutation
    associatedtype State
    
    var initialState: State
}

 

State 는 Reactor Class 에 저장, 원하는 어떤 dataType 이든 사용할 수 있음. 

 

Data Flow

비동기 타임에 State 가 변경되는 경우가 있음.

Action 과 State 사이에 Mutation 을 두어 비동기 처리

Mutation 은 State 를 변경하는 명령 / 작업 단위

Mutation 은 View 에 노출되지 않음

 

 

 

 

 

예시 : Following Btn 

class ProfileViewReactor: Reactor {
    enum Action {
        case follow // 사용자 입력
    }
    
    enum Mutation {
        case setFollowing(Bool) // 상태 변경
    }
    
    struct State {
        var isFollowing: Bool // 뷰 상태
    }
}

 

 

 

 

 

Sample Code 

 

1. ViewController


import UIKit

import ReactorKit
import RxCocoa
import RxSwift

// Conform to the protocol `View` then the property `self.reactor` will be available.
final class CounterViewController: UIViewController, StoryboardView {
    
    @IBOutlet var decreaseButton: UIButton!
    @IBOutlet var increaseButton: UIButton!
    @IBOutlet var valueLabel: UILabel!
    @IBOutlet var activityIndicatorView: UIActivityIndicatorView!
    
    var disposeBag = DisposeBag()
    
    // Called when the new value is assigned to `self.reactor`
    // AppDelegate 에서 reactor 할당됨. 
    func bind(reactor: CounterViewReactor) {
        // Action (View -> Reactor) 
        increaseButton.rx.tap               // Tap event
            .map { Reactor.Action.increase }  // Convert to Action.increase
            .bind(to: reactor.action)         // Bind to reactor.action
            .disposed(by: disposeBag)
        
        decreaseButton.rx.tap
            .map { Reactor.Action.decrease }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
        
        
//         reactor 값이 변하면 해당 함수 호출,
//         reactor 의 state 에 따라 .bind(to:) 을 이용해서 View 의 상태 바꿔줌.
//         State 가 변할 때 value 이외의 값이 변할 수 있으므로 .distinctUntilChanged() 추가.
//         .map 으로 value 값만 이용한다고 알려줌.
//		   State ( Reactor -> View )
		
        reactor.state
            .map { $0.value }   // 10
            .distinctUntilChanged()
            .map { "\($0)" }               // "10"
            .bind(to: valueLabel.rx.text)  // Bind to valueLabel
            .disposed(by: disposeBag)
        
        reactor.state
            .map { $0.isLoading }
            .distinctUntilChanged()
            .bind(to: activityIndicatorView.rx.isAnimating)
            .disposed(by: disposeBag)
        
        reactor.pulse(\.$alertMessage)
            .compactMap { $0 }
            .subscribe(onNext: { [weak self] message in
                let alertController = UIAlertController(
                    title: nil,
                    message: message,
                    preferredStyle: .alert
                )
                alertController.addAction(UIAlertAction(
                    title: "OK",
                    style: .default,
                    handler: nil
                ))
                self?.present(alertController, animated: true)
            })
            .disposed(by: disposeBag)
    }
}

 

 

2. Reactor



import ReactorKit
import RxSwift

final class CounterViewReactor: Reactor {
  // Action is an user interaction
  enum Action {
    case increase
    case decrease
  }

  // Mutate is a state manipulator which is not exposed to a view
  enum Mutation {
    case increaseValue
    case decreaseValue
    case setLoading(Bool)
    case setAlertMessage(String)
  }

  // State is a current view state
  struct State {
    var value: Int
    var isLoading: Bool
    @Pulse var alertMessage: String?
  }

  let initialState: State

  init() {
    self.initialState = State(value: 0, isLoading: false) // start from 0
  }

  // Action -> Mutation
  func mutate(action: Action) -> Observable<Mutation> {
    switch action {
    case .increase:
      return Observable.concat([
        Observable.just(Mutation.setLoading(true)),
        Observable.just(Mutation.increaseValue).delay(.milliseconds(500), scheduler: MainScheduler.instance),
        Observable.just(Mutation.setLoading(false)),
        Observable.just(Mutation.setAlertMessage("increased!")),
      ])

    case .decrease:
      return Observable.concat([
        Observable.just(Mutation.setLoading(true)),
        Observable.just(Mutation.decreaseValue).delay(.milliseconds(500), scheduler: MainScheduler.instance),
        Observable.just(Mutation.setLoading(false)),
        Observable.just(Mutation.setAlertMessage("decreased!")),
      ])
    }
  }

  // Mutation -> State
  func reduce(state: State, mutation: Mutation) -> State {
    var state = state
    switch mutation {
    case .increaseValue:
      state.value += 1

    case .decreaseValue:
      state.value -= 1

    case let .setLoading(isLoading):
      state.isLoading = isLoading

    case let .setAlertMessage(message):
      state.alertMessage = message
    }
    return state
  }
}

 

 

 

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

전수열님 ReactorKit 소개 Presentation : https://www.youtube.com/watch?v=ASwBnMJNUK4

 

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