apple/UIKit & ReactiveX

[ReactorKit] ReactorKit 공부하기 #1

lgvv 2022. 7. 24. 02:39

ReactorKit 공부하기 #1

 

📌 해당 문서는 ReactorKit 3.2.0을 기준으로 하고 있습니다. 

 

오늘은 ReactorKit에 대해서 공부해보려고 합니다.

음,, 사전과제를 수행하면서 정말 오!랜!만!에! RxSwfit를 다시 사용했었는데, 기본기에 더 집중했던 것 때문인지 오랜만에 사용했어도 여렵게 느끼지지 않았습니다.

다만 조금 아쉬웠던 부분이라면, 학습한 디자인 패턴을 SwiftUI 기반의 프로젝트에만 적용하다 당장 잘 작성해야 하는 프로젝트에서 최적의 구조가 어떤 것이지 고민하는 시간이 많아서 개발이 조금 지체되었다는 점,,, 

RxSwfit를 사용한다면 ReactorKit도 알면 좋을 것 같아서 도전 고고!

 

✅ ReactorKit 깃허브 문서

https://github.com/ReactorKit/ReactorKit

 

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

 

✅ Basic Concept

 

ReactorKit 설명!

 

그러니까 ReactorKit은 반응적이고 단방햑적인 Swift 아키텍처를 위한 프레임워크!

 

 

✅ ReactorKit의 기능 개요와 작성 이유가 궁금하다면

https://www.slideshare.net/devxoul/hello-reactorkit

 

Hello, ReactorKit 

Hello, ReactorKit! 👋 Suyeol Jeon https://github.com/devxoul

www.slideshare.net

 

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

 

GitHub - lgvv/DesignPattern: ✨ 디자인 패턴을 공부합니다!

✨ 디자인 패턴을 공부합니다! Contribute to lgvv/DesignPattern development by creating an account on GitHub.

github.com