[ReactorKit] ReactorKit 공부하기 #1
ReactorKit 공부하기 #1
📌 해당 문서는 ReactorKit 3.2.0을 기준으로 하고 있습니다.
✅ 오늘은 ReactorKit에 대해서 공부해보려고 합니다.
음,, 사전과제를 수행하면서 정말 오!랜!만!에! RxSwfit를 다시 사용했었는데, 기본기에 더 집중했던 것 때문인지 오랜만에 사용했어도 여렵게 느끼지지 않았습니다.
다만 조금 아쉬웠던 부분이라면, 학습한 디자인 패턴을 SwiftUI 기반의 프로젝트에만 적용하다 당장 잘 작성해야 하는 프로젝트에서 최적의 구조가 어떤 것이지 고민하는 시간이 많아서 개발이 조금 지체되었다는 점,,,
RxSwfit를 사용한다면 ReactorKit도 알면 좋을 것 같아서 도전 고고!
✅ ReactorKit 깃허브 문서
https://github.com/ReactorKit/ReactorKit
✅ Basic Concept
그러니까 ReactorKit은 반응적이고 단방햑적인 Swift 아키텍처를 위한 프레임워크!
✅ ReactorKit의 기능 개요와 작성 이유가 궁금하다면
https://www.slideshare.net/devxoul/hello-reactorkit
ReactorKit은 Flux와 Reactive Programming의 조합이다! user의 Action과 View의 상태는 observable streams을 통해 각각의 레이어에 전달된다. 이러한 스트림은 단방향 스트림이고, 뷰는 오로지 emit action만 방출할 수 있고 reactor는 state만 방출할 수 있습니다.
✅ 예시 코드를 봅시다!
1. Reactor를 먼저 정의합니다.
//
// CounterViewReactor.swift
// ReactorKitPractice
//
// Created by Hamlit Jason on 2022/07/23.
//
import Foundation
import RxSwift
import RxCocoa
import ReactorKit
class CounterViewReactor: Reactor {
/// 초기 상태를 정의합니다.
let initialState = State()
/// 사용자 행동을 정의합니다.
///
/// 사용자에게 받을 액션
enum Action {
case increase
case decrease
}
/// 처리 단위를 정의합니다.
///
/// 액션을 받았을 때 변화
enum Mutation {
case increaseValue
case decreaseValue
case setLoading(Bool)
}
/// 현재 상태를 기록합니다.
///
/// 어떠한 변화를 받은 상태!
struct State {
var value = 0
var isLoading = false
}
/// Action이 들어온 경우 어떤 처리를 할 것인지 분기
///
/// Mutation에서 정의한 작업 단위들을 사용하여 Observable로 방출
///
/// 액션에 맞게 행동해!
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case .increase:
return Observable.concat([ // concat은 평등하게 먼저 들어온 옵저버블을 순서대로 방출
Observable.just(.setLoading(true)),
Observable.just(.increaseValue).delay(.seconds(1), scheduler: MainScheduler.instance),
Observable.just(.setLoading(false))
])
case .decrease:
return Observable.concat([
Observable.just(.setLoading(true)),
Observable.just(.decreaseValue).delay(.seconds(1), scheduler: MainScheduler.instance),
Observable.just(.setLoading(false))
])
}
}
/// 이전 상태와 처리 단위를 받아서 다음 상태를 반환하는 함수
///
/// mutate(action: )이 실행되고 난 후 바로 해당 메소드를 실행
///
/// 변화에 맞게끔 값을 설정해!
func reduce(state: State, mutation: Mutation) -> State {
var newState = state
switch mutation {
case .increaseValue:
newState.value += 1
case .decreaseValue:
newState.value -= 1
case .setLoading(let isLoading):
newState.isLoading = isLoading
}
return newState
}
}
2. CounterViewController를 정의합니다.
//
// CounterViewController.swift
// ReactorKitPractice
//
// Created by Hamlit Jason on 2022/07/23.
//
import UIKit
import RxSwift
import RxCocoa
import ReactorKit
import SnapKit
class CounterViewController: UIViewController, View {
var disposeBag = DisposeBag()
let counterViewReactor = CounterViewReactor()
lazy var increaseButton = UIButton()
lazy var countLabel = UILabel()
lazy var decreaseButton = UIButton()
lazy var loadingIndicator = UIActivityIndicatorView()
override func viewDidLoad() {
super.viewDidLoad()
setupView()
bind(reactor: counterViewReactor)
}
func bind(reactor: CounterViewReactor) {
bindAction(reactor: reactor)
bindState(reactor: reactor)
}
private func bindAction(reactor: CounterViewReactor) {
increaseButton.rx.tap
.map { CounterViewReactor.Action.increase }
.bind(to: reactor.action)
.disposed(by: disposeBag)
decreaseButton.rx.tap
.map { CounterViewReactor.Action.decrease }
.bind(to: reactor.action)
.disposed(by: disposeBag)
}
private func bindState(reactor: CounterViewReactor) {
reactor.state
.map { "\($0.value)" }
.distinctUntilChanged()
.bind(to: countLabel.rx.text)
.disposed(by: disposeBag)
reactor.state
.map { $0.isLoading }
.distinctUntilChanged()
.bind(to: loadingIndicator.rx.isAnimating)
.disposed(by: disposeBag)
reactor.state
.map { !$0.isLoading }
.distinctUntilChanged()
.bind(to: loadingIndicator.rx.isHidden)
.disposed(by: disposeBag)
}
}
extension CounterViewController {
var margin: CGFloat {
get { return 10.0 }
}
func setupView() {
view.addSubview(increaseButton)
increaseButton.setImage(UIImage(systemName: "plus"), for: .normal)
increaseButton.snp.makeConstraints {
$0.width.height.equalTo(30)
$0.centerY.equalToSuperview()
$0.leading.equalTo(margin)
}
view.addSubview(countLabel)
countLabel.text = "0"
countLabel.snp.makeConstraints {
$0.center.equalToSuperview()
}
view.addSubview(decreaseButton)
decreaseButton.setImage(UIImage(systemName: "minus"), for: .normal)
decreaseButton.snp.makeConstraints {
$0.width.height.equalTo(30)
$0.centerY.equalToSuperview()
$0.trailing.equalTo(-margin)
}
view.addSubview(loadingIndicator)
loadingIndicator.snp.makeConstraints {
$0.centerX.equalToSuperview()
$0.centerY.equalToSuperview().offset(100)
}
}
}
typealias Reactor로 사용할 수 있습니다.
해당 코드 파일은 아래 주소의 ReactorKit 파트에서 확인하실 수 있습니다.
https://github.com/lgvv/DesignPattern