iOS

[iOS] RxSwift, ReactorKit를 이용한 MVVM 구조

빨간체리반지 2020. 9. 25. 15:46

RxSwift

- 비동기 처리를 쉽게 처리할 수 있게 해주는 라이브러리.

- 강력한 Operator.

https://cocoapods.org/pods/RxSwift

 

RxSwift

RxSwift is a Swift implementation of Reactive Extensions

cocoapods.org

 

ReactorKit

- 단방향 데이터 흐름을 가진 반응형 앱을 위한 프레임워크.

https://cocoapods.org/pods/ReactorKit

 

ReactorKit

A framework for reactive and unidirectional Swift application architecture

cocoapods.org

 

(ReactorKit 기본 개념)

 

* Action

View에서 나타나는 Action을 Enum으로 정의한 것

예를 들어, 버튼1 클릭됨 / 버튼2 클릭됨 / 테이블 뷰 스크롤 됨 / 화면이 나타남

 

* Mutation

화면 구현에 있어 필요한 기능을 기본 로직 단위로 분류해 Enum으로 정의한 것

예를 들어, API 호출해서 서버시간 가져오기 / 숫자 더하기 / 글자 변경하기

 

* State

Mutation에 따라 실제로 값이 변경될 변수들을 struct 형태로 묶어둔 것

 

- mutate() 함수를 통해 Action -> Mutation 매핑  // 아래 예시 참고

- reduce() 함수를 통해  Mutation -> State 매핑  // 아래 예시 참고

 

 

예시 프로젝트

 

 

기능 1) PLUS 버튼을 누르면 +1, MINUS 버튼을 누르면 -1 처리

기능 2) 비동기 방식으로 API 호출 후, 결과 처리 (단순히 echo 결과 return 해주는  Postman Echo API 사용)

// View - MainViewController.swift

import UIKit

import RxCocoa

import RxSwift

import ReactorKit

 

class MainViewController: ViewController {

    var disposeBag = DisposeBag()

    let reactor = MainViewReactor()

    

    // 기능 1

    @IBOutlet var plusBtn: UIButton!

    @IBOutlet var minusBtn: UIButton!

    @IBOutlet var label: UILabel!

    // 기능 2

    @IBOutlet var apiLabel: UILabel!

    

    override func viewDidLoad() {

        super.viewDidLoad()

        bind(reactor: reactor)

        

        reactor.action.onNext(.viewDidLoad("hi", 1234))

    }

    

    func bind(reactor: MainViewReactor) {

        

        // set action

        plusBtn.rx.tap

            .map{ MainViewReactor.Action.plus }

            .bind(to: reactor.action)

            .disposed(by: disposeBag)

        

        minusBtn.rx.tap

            .map{ MainViewReactor.Action.minus }

            .bind(to: reactor.action)

            .disposed(by: disposeBag)

        

        

        // detect state changed

        reactor.state.map { $0.number }

            .distinctUntilChanged()

            .map{ "\($0)" }

            .subscribe(onNext: { str in

                self.label.text = str

            })

            .disposed(by: disposeBag)

        

        reactor.state

            .filter { $0.data1 != "" && $0.data2 != 0 }

            .map { "\($0.data1), \($0.data2)" }

            .distinctUntilChanged()

            .subscribe(onNext: {

                self.apiLabel.text = $0

            })

            .disposed(by: disposeBag)

            

    }

}

 

// View Model - MainViewReactor.swift

import Foundation

import ReactorKit

import SwiftyJSON

 

class MainViewReactor: Reactor {

    let initialState: State = State(number: 0)

    

    // represent user actions

    enum Action {

        case viewDidLoad(String, Int)

        case plus, minus

    }

 

    // represent state changes

    enum Mutation {

        case setDataFromApi(String, Int)

        case setNumberTPlusFMinus(Bool)

    }

 

    // represents the current view state

    struct State {

        var data1: String = ""

        var data2: Int = 0

        var number: Int = 0

    }

    

    // Action -> Mutation

    func mutate(action: Action) -> Observable<Mutation> {

        switch action {

        case .viewDidLoad(let data1, let data2):

            return Observable.concat([

                getDataFromApi(data1: data1, data2: data2)

            ])

        case .plus:

            return Observable.concat([

                Observable.just(Mutation.setNumberTPlusFMinus(true)),

            ])

        case .minus:

            return Observable.concat([

                Observable.just(Mutation.setNumberTPlusFMinus(false)),

            ])

        }

    }

    

    // Mutation -> State

    func reduce(state: State, mutation: Mutation) -> State {

        var state = state

        switch mutation {

        case .setDataFromApi(let data1, let data2):

            state.data1 = data1

            state.data2 = data2

        case .setNumberTPlusFMinus(let flag):

            if flag { state.number += 1 }

            else { state.number -= 1 }

        }

        return state

    }

    

    // MARK: Private Functions

    func getDataFromApi(data1: String, data2: Int) -> Observable<Mutation> {

        return Observable<Mutation>.create { observer in

            ApiRequest.shared.getEchoTest(data1: data1, data2: data2)

                .map { $0["args"] }

                .subscribe(onNext: {

                    observer.onNext(.setDataFromApi($0["data1"].stringValue, $0["data2"].intValue))

                    observer.onCompleted()

                })

            return Disposables.create()

        }

    }

}

 

// Model - ApiRequest.swift

import RxSwift

import SwiftyJSON

import Alamofire

 

class ApiRequest {

    static var shared = ApiRequest()

    private init() {}

    

    private let POSTMAN_ECHO_SERVER = "https://postman-echo.com/"

    

    /**

     Postman Echo Test Api

     */

    func getEchoTest(data1: String, data2: Int) -> Observable<JSON> {

        let url = POSTMAN_ECHO_SERVER + "get"

        let params: [String: Any] = [

            "data1": data1,

            "data2": data2,

        ]

        return getJson(url, params: params)

    }

    

    

    // MARK: Private Functions

    private func getJson(_ url: String, params: [String: Any]) -> Observable<JSON> {

        return Observable.create { observer in

            AF.request(url, method: .get, parameters: params).response { response in

                switch (response.result) {

                case .success(let value):

                    observer.onNext(JSON(value!))

                    observer.onCompleted()

                case .failure(let error):

                    observer.onError(error)

                }

            }

            return Disposables.create()

        }

    }

    

}

참고

 

 

Postman Echo

Postman Echo is service you can use to test your REST clients and make sample API calls. It provides endpoints for `GET`, `POST`, `PUT`, various auth mechanisms and other utility endpoints. The documentation for the endpoints as well as example responses c

docs.postman-echo.com