개발/IOS Swift

[ReactorKit#2] Swift ReactorKit Example Counter 카피, 분석하기

doyou1 2021. 10. 25. 00:29
반응형

https://github.com/ReactorKit/ReactorKit/tree/master/Examples/Counter

 

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

결과물

 

ReactorKit 활용을 위해 몇가지 예제를 실습해보고 있다. 그 첫번째 예제이다.

정말 간단한 예제이지만, Action, State, Mutate 등 ReactorKit 활용 방법을 잘 설명해주고 있다.

같이 예제를 완성해보자

 

1. 사전 준비

1.1 Project 생성

1.2 불필요한 file, code 제거

1.2.1 SceneDelegate.swift  삭제

1.2.2 AppDelegate.swift 코드 제거

 

1.2.3 Info.plist 요소 제거

Info.plist -> Information Property List -> Application Scene Manifest -> "Scene Configuration" 제거

 

1.2.4 필요 라이브러리 다운

cocoapod을 통해 "ReactorKit", "RxSwift", "RxCocoa" install

 

* 참고

- https://zeddios.tistory.com/25

 

왕 초보를 위한 CocoaPods(코코아팟) 사용법 (Xcode와 연동)

안녕하세요! 오늘은 CocoaPod사용법에 대해 알려드릴려고해요 :) 저는 CocoaPod 처음에 시작할 때 뭐가 뭔지 몰라서 정말 하나도 몰라서 진짜 어려운거구나...라고 생각했었어요. 하지만 한번 배워 놓

zeddios.tistory.com

 

2. AppDelegate.swift

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window:UIWindow?
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        let viewController = UIApplication.shared.windows.first?.rootViewController as! ViewController
      viewController.reactor = ViewReactor()
      return true
    }

}

- UIWindow 객체를 통해 "rootViewController"인 ViewController의 객체(viewController) 생성

- 해당 객체에 reactor property 추가 // ViewReactor는 Custom Class

 

3. ViewReactor.swift

- ViewReactor.swift 생성

 

- 필요 모듈 import

import ReactorKit
import RxSwift

 

- Action, Mutation, State 선언

class ViewReactor: Reactor {
    // Action is an user interaction
    enum Action {
        case increase	// Count Increase Event
        case decrease	// Count Decrease Event
    }
    
    // Mutate is a state manipulator which is not exposed to a view
    enum Mutation {
        case increaseValue	// Counter Value Increase
        case decreaseValue	// Counter Value Decrease
        case setLoading(Bool)	// is executing Event?
        case setAlertMessage(String)
    }
    
    // State is a current view state
    struct State {
        var value: Int	// Counter value
        var isLoading: Bool	// // is executing Event?
        @Pulse var alertMessage: String?
    }

}

 

- state 초기화

let initialState: State
    
    init() {
        self.initialState = State(
        value: 0, // start from 0
        isLoading: false
        )
    }

 

- mutate() 구현

// Action -> Mutation
    func mutate(action: Action) -> Observable<Mutation> {
        switch action {
        case .increase:		// View로부터의 Action이 increase
            return Observable.concat([
                Observable.just(Mutation.setLoading(true)),
                Observable.just(Mutation.increaseValue)
                    .delay(.milliseconds(500), scheduler: MainScheduler.instance),
                Observable.just(Mutation.setLoading(false)),
                Observable.just(Mutation.setAlertMessage("increased!")),
            ])
        
        case .decrease:		// View로부터의 Action이 decrease
            return Observable.concat([
                Observable.just(Mutation.setLoading(true)),
                Observable.just(Mutation.decreaseValue)
                    .delay(.milliseconds(500), scheduler: MainScheduler.instance),
                Observable.just(Mutation.setLoading(false)),
                Observable.just(Mutation.setAlertMessage("decreased!")),
            ])
        }
    }

 

* Observable이란 ?

http://reactivex.io/documentation/ko/observable.html

 

ReactiveX - Observable

Observable ReactiveX에서 옵저버는 Observable을 구독한다. Obseravable이 배출하는 하나 또는 연속된 항목에 옵저버는 반응한다. 이러한 패턴은 동시성 연산을 가능하게 한다. 그 이유는 Observable이 객체를

reactivex.io

 

- reduce() 구현

 // Mutation -> State
    func reduce(state: State, mutation: Mutation) -> State {
        var state = state
        switch mutation {
        case .increaseValue:
            state.value += 1
        case .decreaseValue:
            state.value -= 1
        case .setLoading(let bool):
            state.isLoading = bool
        case .setAlertMessage(let string):
            state.alertMessage = string
        }
        
        return state
    }

 

- 결과물

//
//  ViewReactor.swift
//  jh_counter
//
//  Created by 추경민 on 2021/10/27.
//

import ReactorKit
import RxSwift

class ViewReactor: Reactor {
    // Action is an user interaction
    enum Action {
        case increase
        case decrease
    }
    
    // Mutate is a state manipulator which is not exposed to a view
    enum Mutation {
        case increaseValue
        case decreaseValue
        case setLoading(Bool)
        case setAlertMessage(String)
    }
    
    // State is a current view state
    struct State {
        var value: Int
        var isLoading: Bool
        @Pulse var alertMessage: String?
    }
    
    let initialState: State
    
    init() {
        self.initialState = State(
        value: 0, // start from 0
        isLoading: false
        )
    }
    
    // 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(.milliseconds(500), scheduler: MainScheduler.instance),
                Observable.just(Mutation.setLoading(false)),
                Observable.just(Mutation.setAlertMessage("increased!")),
            ])
        
        case .decrease:
            return Observable.concat([
                Observable.just(Mutation.setLoading(true)),
                Observable.just(Mutation.decreaseValue)
                    .delay(.milliseconds(500), scheduler: MainScheduler.instance),
                Observable.just(Mutation.setLoading(false)),
                Observable.just(Mutation.setAlertMessage("decreased!")),
            ])
        }
    }
    
    // Mutation -> State
    func reduce(state: State, mutation: Mutation) -> State {
        var state = state
        switch mutation {
        case .increaseValue:
            state.value += 1
        case .decreaseValue:
            state.value -= 1
        case .setLoading(let bool):
            state.isLoading = bool
        case .setAlertMessage(let string):
            state.alertMessage = string
        }
        
        return state
    }
}

 

4. ViewController.swift

- 필요 모듈 import

import ReactorKit
import RxCocoa
import RxSwift

 

- View item 추가 / 이후 main.storyboard에서 연결할 예정

    @IBOutlet var decreaseButton: UIButton!
    @IBOutlet var increaseButton: UIButton!
    @IBOutlet var valueLabel: UILabel!
    @IBOutlet var activityIndicatorView: UIActivityIndicatorView!

 

- DisposeBag 객체 선언

var disposeBag = DisposeBag()

 

* Dispose, DisposeBag이란?

Observable은 subscribe 이후 complete, error 이벤트를 만나기 전까지 "계속" next 이벤트를 발생시킨다. 이는 메모리 누수(릭)로 이어진다. 메모리 누수를 부르는 subscribe된 Observable를 dispose해주는 역할을 한다. DisposeBag은 Observable 객체 하나하나를 dispose해야하는 귀찮음을 해결해준다.

 

- bind() function 추가

- View item tap action과 reactor.action mapping, binding

// Called when the new value is assigned to 'self.reactor'
    func bind(reactor: ViewReactor) {
    
     	// Action
        increaseButton.rx.tap                   // Tap event
            .map { Reactor.Action.increase }    // Convert to Action.increase
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
        
        decreaseButton.rx.tap                   // Tap event
            .map { Reactor.Action.decrease }    // Convert to Action.decrease
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
    
    }

 

- View item value와 reactor.state  mapping, binding

// State
        reactor.state.map { $0.value }  // 10
        .distinctUntilChanged()
        .map { "\($0)" }                // "10"
        .bind(to: valueLabel.rx.text)   // Bind to valueLabel
        .disposed(by: disposeBag)
        
        reactor.state.map { $0.isLoading }
        .distinctUntilChanged()
        .bind(to: activityIndicatorView.rx.isAnimating)
        .disposed(by: disposeBag)

 

* "$0" ?

: 추후 추가

 

* distinctUntilChanged()의 역할?

: 추후 추가

 

 

 reactor.pulse(\.$alertMessage)
            .compactMap { $0 }
            .subscribe(onNext: { [weak self] message in
              let alertController = UIAlertController(
                title: nil,
                message: message,
                preferredStyle: .alert
              )
                alertController.addAction(UIAlertAction(
                    title: "OK",
                    style: .default,
                    handler: nil
                ))
                self?.present(alertController, animated: true)
            })
            .disposed(by: disposeBag)

 

- 결과물

//
//  ViewController.swift
//  jh_counter
//
//  Created by 추경민 on 2021/10/27.
//

import UIKit

import ReactorKit
import RxCocoa
import RxSwift

// Conform to the protocol 'View' then the property 'self.reactor' will be available
class ViewController: UIViewController, StoryboardView{

    @IBOutlet var decreaseButton: UIButton!
    @IBOutlet var increaseButton: UIButton!
    @IBOutlet var valueLabel: UILabel!
    @IBOutlet var activityIndicatorView: UIActivityIndicatorView!
    var disposeBag = DisposeBag()
    
    // Called when the new value is assigned to 'self.reactor'
    func bind(reactor: ViewReactor) {
        // Action
        increaseButton.rx.tap                   // Tap event
            .map { Reactor.Action.increase }    // Convert to Action.increase
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
        
        decreaseButton.rx.tap                   // Tap event
            .map { Reactor.Action.decrease }    // Convert to Action.decrease
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
        // State
        reactor.state.map { $0.value }  // 10
        .distinctUntilChanged()
        .map { "\($0)" }                // "10"
        .bind(to: valueLabel.rx.text)   // Bind to valueLabel
        .disposed(by: disposeBag)
        
        reactor.state.map { $0.isLoading }
        .distinctUntilChanged()
        .bind(to: activityIndicatorView.rx.isAnimating)
        .disposed(by: disposeBag)
        
        reactor.pulse(\.$alertMessage)
            .compactMap { $0 }
            .subscribe(onNext: { [weak self] message in
              let alertController = UIAlertController(
                title: nil,
                message: message,
                preferredStyle: .alert
              )
                alertController.addAction(UIAlertAction(
                    title: "OK",
                    style: .default,
                    handler: nil
                ))
                self?.present(alertController, animated: true)
            })
            .disposed(by: disposeBag)
    }

}

 

5. Main.storyboard

 

위 사진처럼 item 추가 후, @IBOutlet 변수들과 연결

 

 

 

6. 최종 결과물

결과물

 

 

반응형