[iOS] ReactorKit - Counter

Ben·2024년 5월 22일
0

iOS

목록 보기
10/23
post-thumbnail

이번에 ReactorKit GitHub의 Example에 있는 Counter를 직접 구현해보면서 ReactorKit에 대해 공부해보았다.


What?

'단방향 데이터 flow를 가진 반응형 앱을 위한 프레임워크'로 Flux + Reactive Programming이다.

Flux란?
👉 Facebook에서 클라이언트-사이드 웹 어플리케이션을 만들기 위해 사용하는 애플리케이션 아키텍쳐다. 단방향 데이터 흐름을 활용해 뷰 컴포넌트를 구성하는 React를 보완하는 역할을 한다.


Why?

  • Massive View Controller를 피하기 위해
    • 뷰와 로직의 관심사 분리
    • 뷰 컨트롤러의 단순화

  • RxSwift의 장점을 취하기 위해
    • RxSwift를 기반으로 함
    • 모든 RxSwift의 기능 사용 가능

  • 상태 관리의 단순화를 위해
    • 단방향 데이터 흐름
    • 중간 상태를 reduce() 함수로 관리
    • 상태 관리가 간결해짐

Concept

  • View: Rendering 담당
  • Reactor: Business Logic 담당
  • Action: 추상화 된 사용자 입력
  • State: 추상화 된 View 상태

Data Flow

View

  • View는 사용자 입력을 받아서 Action을 Reactor에게 전달
  • Reactor로부터 받은 상태 (State)를 렌더링
  • 버튼, 레이블, 셀, 테이블뷰, 뷰 컨트롤러 등을 모두 View로 취급
import UIKit

import ReactorKit
import RxSwift
import RxCocoa
  
final class CounterViewController: UIViewController, ReactorKit.StoryboardView {
    // UI (생략)
    var disposeBag: DisposeBag()
    
    func bind(reactor: CounterViewReactor) {
        //  Action
        //  decreaseButton / increaseButton 버튼 tap시, Action 발생
        
        decreaseButton.rx.tap                   // Tap event
            .map { Reactor.Action.decrease }    // Convert to Action.decrease
            .bind(to: reactor.action)           // Bind to reactor.action
            .disposed(by: self.disposeBag)
            
        increaseButton.rx.tap
            .map { Reactor.Action.increase }
            .bind(to: reactor.action)
            .disposed(by: self.disposeBag)
            
        //  State
        reactor.state
            .map { $0.value }
            .distinctUntilChanged()
            .map { String(stringLiteral: "\($0)") }
            .bind(to: valueLabel.rx.text)		// // Bind to valueLabel
            .disposed(by: self.disposeBag)
            
        reactor.state
            .map { $0.isLoading }
            .distinctUntilChanged()
            .bind(to: indicator.rx.isAnimating)
            .disposed(by: self.disposeBag)
      }
  }  

Reactor

  • View에서 전달받은 Action에 따라 business logic 수행

  • 상태를 관리하고 상태가 변경되면 View에 전달

  • 대부분의 View는 대응되는 Reactor를 가짐

    Action

    • Action is an user interaction
    • View로부터 받을 Action을 enum으로 정의

    Mutation

    • Mutate is a state manipulator which is not exposed to a view
    • State를 변경하는 '명령' / '작업'의 단위
    • Mutation은 View에 노출되지 않음!
    • ActionState 사이에 Mutation을 둬서 비동기 처리

    State

    • State is a current view state
    • View의 현재 상태를 저장
import Foundation

import ReactorKit
import RxSwift

final class CounterViewReactor: Reactor {
    
    // MARK: - View의 Action 정의
    enum Action {   //  추상화 된 사용자 입력
        //  사용자 입력..
        case increase
        case decrease
    }
    
    // MARK: - Action을 받을 Mutation 정의
    enum Mutation {
        case increaseValue
        case decreaseValue
        case setLoading(Bool)
    }
    
    // MARK: - View의 현재 상태
    struct State {  //  추상화 된 뷰 상태
        //  뷰 상태..
        var value: Int
        var isLoading: Bool
    }
    
    var initialState: State
    
    init() {
        self.initialState = State(value: 0, isLoading: false)
    }
    
    // MARK: - 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(.seconds(1), scheduler: MainScheduler.instance),
                Observable.just(Mutation.setLoading(false))
            ])
            
        case .decrease:
            return Observable.concat([
                Observable.just(Mutation.setLoading(true)),
                Observable.just(Mutation.decreaseValue)
                    .delay(.seconds(1), scheduler: MainScheduler.instance),
                Observable.just(Mutation.setLoading(false))
            ])
        }
    }
    
    // MARK: - Mutation -> State
    func reduce(state: State, mutation: Mutation) -> State {
        
        var newState: State = state
        
        switch mutation {
        case .increaseValue:
            newState.value += 1; break;
        case .decreaseValue:
            newState.value -= 1; break;
        case .setLoading(let isLoading):
            newState.isLoading = isLoading
        }
        
        return newState
    }
}

// MARK: - 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(.seconds(1), scheduler: MainScheduler.instance),
            Observable.just(Mutation.setLoading(false))
        ])
            
    case .decrease:
        return Observable.concat([
            Observable.just(Mutation.setLoading(true)),
            Observable.just(Mutation.decreaseValue)
                .delay(.seconds(1), scheduler: MainScheduler.instance),
            Observable.just(Mutation.setLoading(false))
        ])
    }
}

mutate(action: Action) 함수에서, Action에 대한 분기 처리가 이루어진다.


// MARK: - Mutation -> State
func reduce(state: State, mutation: Mutation) -> State {
        
    var newState: State = state
        
    switch mutation {
    case .increaseValue:
        newState.value += 1; break;
    case .decreaseValue:
        newState.value -= 1; break;
    case .setLoading(let isLoading):
        newState.isLoading = isLoading
    }
        
    return newState
}

reduce(state: State, mutation: Mutation) 함수에서, 이전 상태와 Mutation을 받아서 다음 상태를 반환한다.

전체 소스 코드: https://github.com/Benedicto-H/Counter

profile
 iOS Developer

0개의 댓글