목차
- Basic Concept
- View
1) 뷰 정의하기
2) bind(reactor:) 이란? - Reactor = ViewModel
1) mutate()
2) reduce()
3) transform() - 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.