ReactorKit은 RxSwift와 함께 사용하는 반응형 아키텍처 패턴(Reactive Architecture Pattern)을 기반으로 한 프레임워크이다. MVVM과 유사하지만, ReactorKit은 Reactor라는 개념을 도입하여 상태 관리와 비즈니스 로직을 명확하게 분리한다.
반응형, 단방향 Swift 아키텍쳐이며, Flux + Reactive Programming이라고 정의되어 있음!
- View → 뷰!
- Action → 사용자와 상호작용과 뷰의 상태를 정의한 State를 소유함. Mutation은 Action, State를 브릿징
- Reactor → UI와 분리된 영역으로 State를 관리함. View에게서 제어권을 완전히 가져옴
- State → View가 표시하거나 표시되는 데 사용해야 할 현재 상태를 저장하고 있는 타입
+/- 버튼,activityIndicator,숫자표시 label
// MARK: - Property
let decreaseButton: UIButton = {
let decreaseButton = UIButton()
decreaseButton.setImage(UIImage(systemName: "minus"), for: .normal)
decreaseButton.tintColor = .black
decreaseButton.translatesAutoresizingMaskIntoConstraints = false
return decreaseButton
}()
let increaseButton: UIButton = {
let increaseButton = UIButton()
increaseButton.setImage(UIImage(systemName: "plus"), for: .normal)
increaseButton.tintColor = .black
increaseButton.translatesAutoresizingMaskIntoConstraints = false
return increaseButton
}()
let valueLabel: UILabel = {
let valueLabel = UILabel()
valueLabel.text = "0"
valueLabel.translatesAutoresizingMaskIntoConstraints = false
return valueLabel
}()
let activityIndicator: UIActivityIndicatorView = {
let statusIndicator = UIActivityIndicatorView()
statusIndicator.translatesAutoresizingMaskIntoConstraints = false
return statusIndicator
}()Constraint설정 및addSubview
// MARK: - View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
initConstraint()
}
override func loadView() {
let view = UIView()
self.view = view
view.backgroundColor = .systemBackground
[decreaseButton, increaseButton, valueLabel, activityIndicator].forEach { self.view.addSubview($0) }
}
//MARK: - AutoLayout
func initConstraint() {
NSLayoutConstraint.activate([
decreaseButton.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
decreaseButton.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor, constant: 20),
decreaseButton.heightAnchor.constraint(equalTo: decreaseButton.widthAnchor),
valueLabel.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
valueLabel.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
increaseButton.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
increaseButton.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor, constant: -20),
increaseButton.heightAnchor.constraint(equalTo: increaseButton.widthAnchor),
activityIndicator.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
activityIndicator.topAnchor.constraint(equalTo: valueLabel.safeAreaLayoutGuide.bottomAnchor, constant: 10),
])
}- Result
Action,Mutation,State
class ViewReactor: Reactor {
enum Action { // -> User의 액션을 나타낸다.
case decrease
case increase
}
enum Mutation { // Mutation: 변화 -> 상태 변화를 나타낸다.
case decreaseValue
case increaseValue
case setLoading(Bool)
}
struct State { // 현재 뷰의 상태를 나타낸다.
var value: Int = 0
var isLoading: Bool = false
}
let initialState: State = State()
}func mutate()→ View로부터 Action을 받고Observable<Mutation>을 생성한다.
// 이 메서드는 순수함수 이므로 동기적으로 새로운 State를 return 해야한다.
// 함수 안에서 어떤 side effect들을 수행하면 안됨
func reduce(state: State, mutation: Mutation) -> State {
var newState = state
switch mutation {
case .decreaseValue:
newState.value -= 1
case .increaseValue:
newState.value += 1
case let .setLoading(isLoading):
newState.isLoading = isLoading
}
return newState
}- ViewController.swift
class ViewController: UIViewController, View {
var disposeBag: DisposeBag = DisposeBag()
typealias Reactor = ViewReactor
init(reactor: Reactor) {
super.init(nibName: nil, bundle: nil)
self.reactor = reactor
}
required init?(coder: NSCoder) {
//fatalError("init(coder:) has not been implemented")
super.init(coder: coder)
}
...
}- SceneDelegate.swift
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let reactor = ViewReactor()
window?.rootViewController = ViewController(reactor: reactor)
guard let _ = (scene as? UIWindowScene) else { return }
}func bind()→ 뷰와 리액터 사이의 action과 state를 바인드 하기 위한 메서드
func bind(reactor: ViewReactor) {
// action
decreaseButton.rx.tap
.map{ Reactor.Action.decrease }
.bind(to: reactor.action)
.disposed(by: disposeBag)
increaseButton.rx.tap
.map{ Reactor.Action.increase }
.bind(to: reactor.action)
.disposed(by: disposeBag)
// State
reactor.state.map{ $0.value } // 첫번째 map -> value를 추출하는 역할
.distinctUntilChanged()
.map{ String($0) } // Int값을 String으로 바꾸는 역할
.bind(to: valueLabel.rx.text)
.disposed(by: disposeBag)
reactor.state.map{ $0.isLoading }
.distinctUntilChanged()
.bind(to: activityIndicator.rx.isAnimating)
.disposed(by: disposeBag)
}