ReactorKit
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