ReactorKit으로 단방향 반응형 앱 만들기 - 전수열
-
Massive View Controller를 피하기 위해
Why?
- 뷰와 로직의 관심사 분리
- 뷰 컨트롤러가 단순해짐
-
RxSwift의 장점을 모두 취한다.
- ReactorKit은 RxSwift를 기반으로 함
- 모든 RxSwift 기능을 사용 가능
-
상태 관리가 쉽다.
- 단방향 데이터 흐름
- 중간 상태를 reduce() 함수(pure function)로 관리
- pure function(순수 함수)
- 상태 관리가 간결해짐
View
- 사용자 입력을 받아서 Reactor에 전달
- Reactor로부터 받은 상태를 렌더링
- 뷰 컨트롤러, 셀, 컨트롤 등을 모두 View로 취급
Reactor
- View에서 전달받은 Action에 따라 로직 수행
- 상태를 관리하고 상태가 변경되면 View에 전달
- 대부분의 View는 대응되는 Reactor를 가짐
Mutation
- Action이 State를 바로 변경하지는 않음
- 비동기 타임에 State가 변경되는 경우가 있음
- Action과 State 사이에 Mutation을 둬서 비동기 처리
- Mutation은 State를 변경하는 명령/작업 단위
- Mutation은 View에 노출되지 않음
mutate() - 변이
func mutate(Action) -> Observable<Mutation> {}
reduce() - 환원
func reduce(State, Mutation) -> State {}
Reactor
import ReactorKit
import RxSwift
final class CounterViewReactor: Reactor {
enum Action {
case increase
case decrease
}
enum Mutation {
case increaseValue
case decreaseValue
case setLoading(Bool)
}
struct State {
var value: Int = 0
var isLoading: Bool = false
}
let initialState: State = State()
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case .increase:
return Observable.concat([
Observable.just(Mutation.setLoading(true)),
Observable.just(Mutation.increaseValue)
.delay(1, scheduler: MainScheduler.instance),
Observable.just(Mutation.setLoading(false))
])
case .decrease:
return Observable.concat([
Observable.just(Mutation.setLoading(true)),
Observable.just(Mutation.decreaseValue)
.delay(1, scheduler: MainScheduler.instance),
Observable.just(Mutation.setLoading(false))
])
}
}
func reduce(state: State, mutation: Mutation) -> State {
var newState = state
switch mutation {
case .increaseValue:
newState.value += 1
case .decreaseValue:
newState.value -= 1
case .setLoading(isLoading):
newState.isLoading = isLoading
}
return newState
}
}
ViewController
import ReactorKit
import RxSwift
import RxCocoa
final class CounterViewController: UIViewController, StoryboardView {
@IBOutlet var increaseButton: UIButton!
@IBOutlet var decreaseButton: UIButton!
@IBOutlet var valueLabel: UILabel!
@IBOutlet var activityIndicatorView: UIActivityIndicatorView!
var disposeBag = DisposeBag()
func bind(reactor: CounterViewReactor) {
// Action
increaseButton.rx.tap
.map { Reactor.Action.increase }
.bind(to: reactor.action)
.disposed(by: disposeBag)
decreaseButton.rx.tap
.map { Reactor.Action.decrease }
.bind(to: reactor.action)
.disposed(by: disposeBag)
// State
reactor.state.map { $0.value }
.distinctUntilChanged()
.map { "\($0)" }
.bind(to: valueLabel.rx.text)
.disposed(by: disposeBag)
reactor.state.map { $0.isLoading }
.distinctUntilchanged()
.bind(to: activityIndicatorView.rx.isAnimating)
.disposed(by: disposeBag)
}
}
AppDelegate
...
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let counterVC = window?.rootViewController as? CounterViewController
let counterViewReactor = CounterViewReactor()
counterVC?.reactor = counterViewReactor
return true
}
...
댓글