12 February 2017

MVVM

MVVM 구조가 잘 작성되어 있는 예제가 많이 없다보니 어떻게 구조를 작성해야할 지 시작하기 어렵습니다. 그 와중에 킥스타터에서 자사 앱을 오픈소스로 공개했습니다. 여기에서 제가 주목한 점은 MVVM 아키텍처를 적용한 프로젝트였다는 점입니다.

그전에 MVVM을 한번 살펴보도록 합시다.


-출처 : MSDN

Model은 ViewModel에 notification를 전달하고, ViewModel은 View에 notification를 전달합니다. View는 ViewModel에 명령을 전달하고, ViewModel은 Model을 업데이트 합니다.

ViewModel은 View에서 명령을 Input으로 받고, View에 전달할 notification를 output으로 내보낼 수 있습니다.

따라서 ViewModel은 input과 output을 가집니다. 이것을 코드로 나타내면 다음과 같이 작성할 수 있습니다.

  protocol ViewModelInput {}

  protocol ViewModelOutput {}

  protocol ViewModelType {
    var inputs: ViewModelInput { get }
    var outputs: ViewModelOutput { get }
  }

  class ViewModel: ViewModelInput, ViewModelOutput, ViewModelType {
    init() {}
    
    var inputs: ViewModelInput { return self }
    var outputs: ViewModelOutput { return self }
  }

View에서 ViewModel에 inputs과 outputs을 통해 접근해야 합니다.

ViewModel은 View에서 명령을 Input으로 받으므로, ViewModelInput 프로토콜에 View에서 보낼 명령을 정의합니다.

  protocol ViewModelInput {
    func request1()
    func request2()
    func request3()
  }

ViewModel 클래스는 ViewModelInput 프로토콜에 정의된 함수를 구현합니다.

  extension ViewModel {
    func request1() {
      ...
    }
    func request2() {
      ...
    }
    func request3() {
      ...
    }
  }



이제 ViewModel에서 View에 notification를 전달할 output을 만들어 봅시다. 먼저 Model에서 ViewModel에 notification을 전달하므로, Model을 작성합니다.

  struct Model {
    let number = Variable(100)
  }

ViewModel은 Model의 number의 notification을 View에 보내도록 ViewModelOutput 프로토콜에 정의하고 ViewModel에 구현합니다.

  protocol ViewModelOutput {
    var number: Observable<Int> { get }
  }

  class ViewModel: ViewModelInput, ViewModelOutput, ViewModelType {
    let model: Model
    init() {
      model = Model()

      self.number = model.number.asObservable()
    }

    let number: Observable<Int>

    var inputs: ViewModelInput { return self }
    var outputs: ViewModelOutput { return self }
  }



View는 ViewModelOutput 프로토콜에 정의된 number를 구독하여 화면에 반영합니다.

  class ViewController: UIViewController {

    var viewModel: ViewModel!
    let disposeBag = DisposeBag()

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

    func bind() {
      viewModel.outputs.number
        .subscribe(onNext: { number in
            print(number)
          })
        .addDisposableTo(disposeBag)

      btn.rx.tap
        .subscribe(onNext: viewModel.inputs.request1)
        .addDisposableTo(DisposeBag)
    }
  }

정리

Model에서 발생한 notification은 ViewModel의 init에서 다루며, ViewModel의 output을 View가 구독합니다. 그리고 View에서 ViewModel의 input으로 명령으로 보내고, ViewModel은 명령을 받아 Model에 반영합니다.

POP를 이용하여 ViewModel의 input과 output 역할을 명확하게 해줍니다.

참고자료