목차

  1. Basic Concept
  2. View
    1) 뷰 정의하기
    2) bind(reactor:) 이란?
  3. Reactor = ViewModel
    1) mutate()
    2) reduce()
    3) transform()
  4. Advanced
    - Global States
    - View Comminucation

 

 

1. Basic Concept

  • ReactorKit is a combination of Flux and Reactive Programming.
  • 유저의 액션과 뷰 상태는 observable stream을 통해 각 레이어로 전달됨
  • 이 스트림들은 한방향이다: 뷰는 오직 액션만 방출할 수 있고 리액터는 상태만 방출할 수 있다.

 

2. View

  • 뷰는 데이터를 보여주는 역할.
  • 뷰는 사용자의 인풋을 액션스트림에 바인딩하고, 뷰 상태를 UI 컴포넌트에 바인딩함.
  • 뷰 레이어에는 비지니스 로직이 없다. 뷰는 단지! 액션 스트림과 상태 스트림을 보여주는 방법을 정의하는 것

1) 뷰 정의하기

  • 뷰를 정의하기 위해서, 존재하는 클래스에 View 프로토콜을 컨펌시키자. 그러면 reactor라는 프로퍼티를 자동으로 가질 수 있다.
  • 이 프로퍼티는 일반적으로 view의 외부에 설정된다. (뷰 외부에서 reator 주입시키는 것이 일반적)
class ProfileViewController: UIViewController, View {
  var disposeBag = DisposeBag()
}

profileViewController.reactor = UserViewReactor() // inject reactor(reactor 주입)

 

2) bind(reactor:) 란?

  • => 뷰와 리엑터 사이의 액션 스트림상태 스트림을 바인드하기 위한 메서드
  • reactor 프로퍼티가 변경되면, bind(reacor:)가 호출된다.
func bind(reactor: ProfileViewReactor) {
  // action (View -> Reactor)
  refreshButton.rx.tap.map { Reactor.Action.refresh }
    .bind(to: reactor.action)
    .disposed(by: self.disposeBag)

  // state (Reactor -> View)
  reactor.state.map { $0.isFollowing }
    .bind(to: followButton.rx.isSelected)
    .disposed(by: self.disposeBag)
}

 

NOTE
ReatorKit에서 말하는 액션 스트림상태 스트림은 아래 Reactor 클래스의 구현부를 보면 알 수 있다.
뷰에 있는 UI 컴포넌트에 rx를 이용하여 해당 Observable을 reactor의 action이 구독한다. 
그리고, reactor.state인 Observable을 뷰의 UI 컴포넌트가 구독한다.

이와 같이 단방향으로 액션 스트림과 상태스트림으로 Action과 State 데이터가 이동한다.

 

3. Reactor = ViewModel

  • Reactor는 뷰의 상태를 관리하는 UI 독립적인 계층
  • Reactor의 가장 중요한 역할은 뷰로부터 제어 흐름을 분리하는 것
  • 모든 뷰는 해당 Reacor를 가지고 있으며, 모든 로직을 그 Reactor에 위임한다.
  • Reactor는 뷰의 의존성이 없기 때문에 테스트하기가 용이하다.

리엑터를 정의하기 위해서는 Reactor 프로토콜을 준수하자!

세개 타입 Action, Mutation, State. + initialState 프로퍼티 필수!

Action은 사용자의 인터렉션, State는 뷰의 상태

class ProfileViewReactor: Reactor {
  // represent user actions (유저 액션을 나타냄)
  enum Action {
    case refreshFollowingStatus(Int)
    case follow(Int)
  }

  // represent state changes (상태 변화를 나타냄)
  enum Mutation {
    case setFollowing(Bool)
  }

  // represents the current view state (현재 뷰 상태를 나타냄)
  struct State {
    var isFollowing: Bool = false
  }

  let initialState: State = State()
}

 

(중요)

리엑터는 2단계로 액션 스트림을 상태 스트림으로 변경한다:  mutate() reduce().

 

1) mutate()

View로부터 Action을 받고, Observable<Mutation>을 생성한다

func mutate(action: Action) -> Observable<Mutation>

 

비동기 오퍼레이션 또는 API 호출과 같은 모든 사이드 이펙트는 이 메소드 안에서 수행된다

func mutate(action: Action) -> Observable<Mutation> {
  switch action {
  case let .refreshFollowingStatus(userID): // receive an action
    return UserAPI.isFollowing(userID) // create an API stream
      .map { (isFollowing: Bool) -> Mutation in
        return Mutation.setFollowing(isFollowing) // convert to Mutation stream
      }

  case let .follow(userID):
    return UserAPI.follow()
      .map { _ -> Mutation in
        return Mutation.setFollowing(true)
      }
  }
}

 

2) reduce()

기존 State과 Mutation으로부터 새로운 State를 생성

func reduce(state: State, mutation: Mutation) -> State

이 메소드는 순수함수(pure function)이므로 동기적으로 새로운 State를 리턴해야한다. 이 함수안에서 어떤 side effect들을 수행하지 말자.

func reduce(state: State, mutation: Mutation) -> State {
	var state = state 	// create a copy of the old state
    switch mutation {
    case let .setFollowing(isFollowing):
    	state.isFollowing = isFollowing // manipulate the state, create a new state
        return state	// return the new state
    }
}

 

3) transform()

transform()은 각 스트림을 변형한다. 세 transform() 함수가 존재한다:

func transform(action: Observable<Action>) -> Observable<Action>
func transform(mutation: Observable<Mutation>) -> Observable<Mutation>
func transform(state: Observable<State>) -> Observable<State>

 

다른 observable 스트림을 결합하고 변경하려면 이 함수들을 구현하자. 

예를들어, transform(mutation:)은 global event stream을 mutation stream에 결합하기 위한 좋은 방법이다. Global state는 아래에서 자세히 살펴보자.

 

이 메소드는 디버깅을 위한 목적으로 사용될 수 있다.

func transform(action: Observable<Action>) -> Observable<Action> {
  return action.debug("action") // Use RxSwift's debug() operator
}

 

 

4. Advanced

Global States

  • Reactor의 일반적 Action → Mutation → State 흐름에 global state는 없으므로, global state 변경을 위해서 transform(muation:)을 사용해야 한다.
  • 예) 현재 인증된 유저들을 저장하는 global BehaviorSubject가 있다고 가정. 만약 currentUser가 변경되었을 때 Mutation.setUser(User?)를 방출하기를 원한다면 아래와 같이 작성. 그리고 그 mutation은 뷰가 액션을 리엑터에게 보낼 때 각 시간에 방출될 것이다.
var currentUser: BehaviorSubject<User> // global state

func transform(mutation: Observable<Mutation>) -> Observable<Mutation> {
  return Observable.merge(mutation, currentUser.map(Mutation.setUser))
}

 

View Communication

  • ReactorKit은 global app state를 정의하고 있지 않으므로, global state를 관리하기 위해서 BehaviorSubject, PublishSubject, reactor를 사용할 수 있다.
  • 여러개의 뷰들을 커뮤니케이션하기 위해서는 callback 클로저나 delegate 패턴을 사용하는 것이 일반적.
  • ReactorKit은 reactive extension을 사용하는것을 추천한다.
  • 주요 목적은 커스텀한 뷰에 대한 커뮤니케이션을 해결하기 위한 것.

 

예) ChatViewController는 메시지를 보여주는 뷰컨트롤러. ChatViewController는 MessageInputView를 가지고 있다.

유저가 MessageInputView의 보내기 버튼을 탭하면, 그 텍스트는 ChatViewController에 보내질 것이고, ChatViewController는 리엑터의 액션과 바인드될 것이다. MessageView의 reactor extension의 예이다:

extension Reactive where Base: MessageInputView {
  var sendButtonTap: ControlEvent<String> {
    let source = base.sendButton.rx.tap.withLatestFrom(...)
    return ControlEvent(events: source)
  }
}

ChatViewController에서 messageinputView extension를 아래와 같이 사용하자:

messageInputView.rx.sendButtonTap
  .map(Reactor.Action.send)
  .bind(to: reactor.action)

 

 

 

 

 

Ref.

https://github.com/ReactorKit/ReactorKit

+ Recent posts