관리 메뉴

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

[Architecture] RxSwift, MVVM 구조 코드 본문

Architecture (swift)

[Architecture] RxSwift, MVVM 구조 코드

jake-kim 2020. 9. 26. 23:31

핵심

  • subscribe할 경우, onNext를 써서 self...이렇게 접근하는 것을 지양
  • viewModel에서 Observer / Observable이 아닌 그냥 변수로 선언하는 것은 지양 (flag값도 그냥 flag = false로 쓰면 좋지 않음): test case작성 시, Observer변수가 테스트하기 용이

간단한 RxSwift, MVVM구조 설계

- 토글 버튼을 클릭하면 아래 label의 텍스트가 변하는 로직

핵심은, ViewController에서는 Input에 대한 바인딩 / ViewModel에서는 transform에서 비즈니스 로직을 처리하여 Ouput으로 반환

 

1) ViewModel프로토콜 생성

단, 프로젝트 생성시 UnitTest를 체크해야함 (체크 안하고 RxTest를 프레임워크에 추가하면 오류발생)

protocol ViewModel: class {
    associatedtype Input
    associatedtype Output

    func transform(input: Input) -> Output
}

2) ViewModel구현

- 구현 전

import UIKit
import RxSwift
import RxCocoa

class ToggleVM: ViewModel {

    struct Input {

    }

    struct Output {

    }

    func transform(input: Input) -> Output {

    }
}

- 구현 후

class ToggleVM: ViewModel {

    let bag = DisposeBag()

    struct Input {
        var didTapBtnToggle: Observable<Void>
    }

    struct Output {
        var toggleCount: Driver<Int>
    }

    func transform(input: Input) -> Output {

        let toggleCount = BehaviorRelay(value: 0)

        input.didTapBtnToggle.bind(onNext: { _ in
            toggleCount.accept(toggleCount.value + 1)
        }).disposed(by: bag)

        return Output(toggleCount: toggleCount.asDriver(onErrorJustReturn: 0))
    }
}

3) ViewController구현

  • viewModel, input, output변수 모두 함수 밖에서 초기화
  • input 바인딩: 함수 밖에서 lazy var로 초기화
  • output 바인딩: 함수 안에서 바인딩
import Foundation
import RxSwift
import RxCocoa
import UIKit

final class ToggleVC: UIViewController {

    @IBOutlet weak var btnToggle: UIButton!
    @IBOutlet weak var lblCount: UILabel!

    let bag = DisposeBag()
    let viewModel = ToggleVM()

    private lazy var input = ToggleVM.Input(didTapBtnToggle: self.btnToggle.rx.tap.asObservable())
    private lazy var output = self.viewModel.transform(input: input)

    override func viewDidLoad() {
        super.viewDidLoad()
        bindViewModel()
    }

    private func bindViewModel() {
        output.toggleCount
            .map { String($0) }
            .drive(lblCount.rx.text)
            .disposed(by: bag)
    }
}

- MVVM 구조 설계 출처: github.com/sergdort/CleanArchitectureRxSwift#application-1

ViewModel에 Dependencies 담기

기본은 Input, ouput구조이지만, Dependencies를 주어, 더욱 좋은 구성

(dependencies는 router같은 외부에서 주입하도록 아래와 같이 설계하는게 베스트 모델)

class OptionVM: ViewModel {
    struct Input {

    }

    struct Output {

    }

    struct Dependencies {

    }

    let dependencies: Dependencies

    init(dependencies: Dependencies) {
        self.dependencies = dependencies
    }

    func transform(input: Input) -> Output {
        return Output()
    }
}

ViewController에서 사용방법

private func setUpBindings() {

        let tapGesture = UITapGestureRecognizer()
        imgBackground.addGestureRecognizer(tapGesture)

        let input = OptionVM.Input.init(
            viewWillAppear: rx.viewWillAppear.asObservable().map { _ in },
            didTapButton: btnStart.rx.tap.asObservable(),
            didTapImgBackground: tapGesture.rx.event.map { _ in }.asObservable()
        )
        
        let output = input |> viewModel.transform(input: input)

        output.goToSelectOption
            .drive(rx.routeToSelectOption)
            .disposed(by: bag)

        output.error
            .drive(rx.showErrorDialog)
            .disposed(by: bag)
    }

RxSwiftExt프레임 워크를 사용하여, 비동기를 동기적으로 처리하는 방법

pod 'RxSwiftExt'

import RxSwiftExt

// 설치
pod 'RxSwiftExt'

// import
import RxSwiftExt

ViewModel에서 transform내부, RxSwiftExt활용

- 비동기적인 것들을 마치, 동기적인 코드로 짜듯이 이어나갈 수 있음 (핵심은 materialize()메소드)

  • materilize(): Observable객체를 변환 -> error이벤트가 발생하지 않으며, Event객체로 랩핑해주는 메소드 (.next, .compete, .error 생성)
  • elements(): Event값 중, .next 방출 값
  • errors(): Event 값 중, .error 방출 값
func transform(with dependencies: Dependencies) -> (Input) -> Output {
        return { input in

            // viewWillAppear
            let viewWillAppearEvent = input.viewWillAppear.share().materialize()
            let viewWillAppearSuccess = viewWillAppearEvent.elements()
            let viewWillAppearError = viewWillAppearEvent.errors()

            // requestAuthorization
            let acceptTerms = viewWillAppearSuccess
                .flatMap { _ in
                    Observable.of(UNUserNotificationCenter.current().getNotificationSettings(completionHandler:)).materialize()
            }
            let acceptTermsSuccess = acceptTerms.elements()
            let acceptTermsError = acceptTerms.errors()

            // didTapBtnStart
            let didTapButtonStartEvent = input.didTapButton.share().materialize()
            let didTapButtonStartNext = didTapButtonStartEvent.elements()
            let didTapButtonStartError = didTapButtonStartEvent.errors()

            // didTapImgBackground
            let didTapImgBackgroundEvent = input.didTapImgBackground.share().materialize()
            let didTapImgBackgroundNext = didTapImgBackgroundEvent.elements()
            let didTapImgBackgroundError = didTapImgBackgroundEvent.errors()

            // goToSelectOption
            let goToSelectOption = Observable.merge(
                didTapButtonStartNext.asObservable(),
                didTapImgBackgroundNext.asObservable()
            )

            // error
            let errors = Observable.merge(
                viewWillAppearError,
                acceptTermsError,
                didTapButtonStartError,
                didTapImgBackgroundError
            )

            return Output(
                goToSelectOption: goToSelectOption.asDriver { _ in .never()},
                error: errors.map { $0.localizedDescription }.asDriver { _ in .never() }
            )
        }

 

Comments