Kickstarter-iOSのViewModelの作り方がウマかった


追記 2020-12-01

この記事も相当古いものになりましたが、
私自身は現在もMVVMのパターンを利用していますが、VM-Vの間ではFluxパターンを活用しています。

iOSアプリにおけるFluxの難しさと開発を加速させる”store-pattern”

はじめに

22日目担当のmuukiiです。
最近Swiftを2スペで書くことにハマってます😎

先日KickstaterのiOSアプリがオープンソースとして公開されましたね!
GitHub - kickstarter/ios-oss: Kickstarter for iOS. Bring new ideas to life, anywhere.

最初はちら見した程度だったのですが、MVVM + ReactiveCocoaということに気づき興味が湧いたので色々観察していました。

観察した中で良いな!と思ったことが幾つかあったのですが、今回はその一つを共有します。

(私自身はRxSwiftを使用しているため記事の中ではRxSwiftに置き換えてあります。)

ViewModelにprotocolをつかってinputs, outputsを定義している

Kickstarter-iOSでは大体以下のようなテンプレートによりViewModelが定義されていました。

protocol MyViewModelInputs {

}

protocol MyViewModelOutputs {

}

protocol MyViewModelType {
  var inputs: MyViewModelInputs { get }
  var outputs: MyViewModelOutputs { get }
}

final class MyViewModel: MyViewModelType, MyViewModelInputs, MyViewModelOutputs {

  // MARK: - Properties

  var inputs: MyViewModelInputs { return self }
  var outputs: MyViewModelOutputs { return self }

  // MARK: - Initializers


  // MARK: - Functions

}

ViewControllerでは以下のような定義でViewModelを扱うようです。

class MyViewController: UIViewController {
  let viewModel: MyViewModelType
}

MyViewModelType にはinputs, outputsしか定義されていないので、
ViewModelへのアクセス時には必ず (Viewからの) 入力なのか出力なのかを選ぶようにさせているのです。

これにどのようなメリットがあるかというと、ViewModelのプロパティ宣言が AnyObserver or Observable だけになるのであれば、入力か出力は見てわかります。
ですが、PublishSubjectVariable は要件的に使わざるを得ないときはあります。
そのときに入力か出力がわかりにくくなってしまいます。
(プロパティのネーミング次第でどうにかなりそうではありますが。)

そこで、上記のようなViewModelの定義にして、入力か出力をわかりやすくすることができます。

実際にプロパティを宣言した場合以下のようになります。


protocol ProfileViewModelInputs {
  var refresh: PublishSubject<Void> { get }
}

protocol ProfileViewModelOutputs {
  var name: String { get }
  var age: String { get }
    var isOnline: Variable<Bool> { get }
}

protocol ProfileViewModelType {
  var inputs: ProfileViewModelInputs { get }
  var outputs: ProfileViewModelOutputs { get }
}

final class ProfileViewModel: ProfileViewModelType, ProfileViewModelInputs, ProfileViewModelOutputs {

  // MARK: - Properties

  var inputs: ProfileViewModelInputs { return self }
  var outputs: ProfileViewModelOutputs { return self }

  let name: String = ...
  let age: String = ...
    let isOnline: Variable<Bool> = ...

  let refresh: PublishSubject<Void> = ...
}
let viewModel: ProfileViewModelType

viewModel.outputs.name ...
viewModel.outputs.age ...
viewModel.outputs.isOnline ...

//======//

viewModel.inputs.refresh.onNext()

結構いい

私はこのアプローチが気に入ったので自分の開発プロジェクトでも導入し始めています。
自然とprotocolが用いられるのでテストを書くときにも良いのではないかと。

いかがでしょうかー?😉

補足ですが protocol extensionを使うと以下のように定義できますが、
inputs outputs 以降の補完が効かなくなるので現時点ではおすすめしません。

protocol MyViewModelType {
  var inputs: MyViewModelInputs { get }
  var outputs: MyViewModelOutputs { get }
}

extension MyViewModelType where Self: MyViewModelInputs {
  var inputs: MyViewModelInputs { return self }
}

extension MyViewModelType where Self: MyViewModelOutputs {
  var outputs: MyViewModelOutputs { return self }
}

毎回書くのが手間なのでスクリプトつくりました。

また、この形式だと毎回書くのがちょっとだけ辛いので、
簡単なシェルスクリプトをひとまず用意しました。

私はAlfredのworkfowに入れて生成するようにしています。

gen_viewmodel.sh · GitHub

以上です。ありがとうございましたー!