관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] 1. ReactorKit - 개념 본문

Architecture (swift)/ReactorKit

[iOS - swift] 1. ReactorKit - 개념

jake-kim 2021. 11. 30. 02:33

1. ReactorKit - 개념

2. ReactorKit - 테스트 방법 (Storyboard 사용, IBOutlet 테스트 방법)

3. ReactorKit - `TaskList 구현`, 템플릿 (template), 비동기 처리 transform(mutation:)

4. ReactorKit - `TaskEdit 구현`, 화면전환, 데이터 전달

 

* 해당 코드는 ReactorKit git repository 코드를 참고하였습니다.

ReactorKit 개념

  • RxSwift의 강점인 비동기 처리에 편리한 코드
  • API를 통해 앱에서 연속적인 페이지 로드할 때 이전 페이지를 기록해놓아야 하듯이, 이전 페이지를 기록하는 `상태`컴포넌트가 따로 관리되게끔 설계된 구조
  • `상태`라는 컴포넌트 추상화 개념이 적용되어 있고, 컴포넌트 간 결합도가 낮기 때문에 테스트하기에 용이한 구조
  • MVVM과 같은 아키텍처는 규격화된게 없어서 개발자, 회사마다 모두 달라 일관적이지 않은 반면에 ReactorKit은 형식이 갖추어진 형태

CocoaPods

pod 'ReactorKit'
pod 'RxCocoa'

컴포넌트

https://medium.com/styleshare/reactorkit-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-c7b52fbb131a

  • UI에 해당하는 View와 UI에 반응하여 비즈니스 로직을 처리하는 Reactor로 구성
  • Reactor에 View의 Action을 미리 정의해놓고, 해당 action을 처리하여 다시 View에 State값을 넘기는 것
  • View에서는 인터렉터 이벤트들을 Reactor의 Action값으로 넘기고, reactor의 state값을 구독하고 UI업데이트 하는 것

View

  • UI가 있고, UI들의 action을 reactor에 넘기고, reactor의 state를 구독하고 있는 형태

ex) increaseButton 예시

- ReactorKit 내부적으로 호출되는 bind(reactor:) 메소드에서 바인딩 수행

  - bindAction(_:): View에서 Reactor로 이벤트 방출

  - bindState(_:): Reactor에서 바뀐 state들을 View에서 구독

  - storyboard 이용 시 StoryboardView 구현

import UIKit
import RxSwift
import RxCocoa
import ReactorKit

class CounterViewController: UIViewController, StoryboardView {
    
    var disposeBag = DisposeBag()
    
    @IBOutlet weak var countLabel: UILabel!
    @IBOutlet weak var increaseButton: UIButton!
    
    func bind(reactor: CounterViewReactor) {
        bindAction(reactor)
        bindState(reactor)
    }
    
    private func bindAction(_ reactor: CounterViewReactor) {
        increaseButton.rx.tap
            .map { Reactor.Action.increase }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
    }

    private func bindState(_ reactor: CounterViewReactor) {
        reactor.state
            .map { String($0.value) }
            .distinctUntilChanged()
            .bind(to: countLabel.rx.text)
            .disposed(by: disposeBag)
    }
}

Reactor

  • Action
    • View로부터 받을 Action을 enum으로 정의
  • Mutation
    • View로부터 action을 받은 경우, 해야할 작업단위들을 enum으로 정의
  • State
    • 현재 상태를 기록하고 있으며, View에서 해당 정보를 사용하여 UI업데이트 및 Reactor에서 image를 얻어올때 page정보들을 저장
  • mutate(action:) -> Observable<Mutation>
    • Action이 들어온 경우, 어떤 처리를 할것인지 Mutation에서 정의한 작업 단위들을 사용하여 Observable로 방출
    • 해당 부분에서, RxSwift의 concat 연산자를 이용하여 비동기 처리가 유용
    • concat 연산자: 여러 Observable이 배열로 주어지면 순서대로 실행
      (이 밖의 merge, combineLatest, withLatestFrom, zip 사용 개념 참고)
  • reduce(state:mutation:) -> State
    • 현재 상태(state)와 작업 단위(mutation)을 받아서, 최종 상태를 반환
    • mutate(action:) -> Observable<Mutation>이 실행된 후 바로 해당 메소드 실행
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이 들어온 경우, 어떤 처리를 할건지 분기
    func mutate(action: Action) -> Observable<Mutation> {
        switch action {
        case .increase:
            return Observable.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))
            ])
        }
    }
    
    // 이전 상태와 처리 단위를 받아서 다음 상태를 반환하는 함수
    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
    }
}

* 전체 소스 코드: https://github.com/JK0369/ExReactorKit/tree/BaseComponent

 

* 참고

- https://medium.com/styleshare/reactorkit-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-c7b52fbb131a

Comments