iOS
MVVM
デザインパターン
Swift
swift4

[Swift4]今更ながらMVVMのチュートリアルの翻訳をして理解を深めました

MVVMについてのおさらい

本記事は下記のページの翻訳になります。

Design Patterns by Tutorials: MVVM

ちなみに先に言っておきますが、
私はMVVMは面倒臭い派でMVCの方が好きです。

近年、iOS開発でMVVMがよく持ち上げられているのであえてこの記事を通してMVVMの理解を深めたいと思って読んでいました。
MVVMをこれから学ぶ人にも共有したくはありますが、
どちらかといえば「MVVMめっちゃいけてるやんって思っている方」にこそ日本語で読んでいただきたいと思っての共有です。

Design Patterns by Tutorials: MVVM

これは、我々の本「チュートリアルによるデザインパターン」の第10章「Model-View-ViewModel」から抜粋したものです。あなたが開発する言語やプラットフォームに関係なく、デザインパターンは非常に便利です。すべての開発者は実装方法を知っている必要があります。 それはあなたがこの本で学ぶことです。 楽しいですよ。

Model-View-ViewModel(MVVM)は、オブジェクトを3つの異なるグループに分ける構造設計パターンです。

  • Modelはアプリケーションデータを保持します。 通常はstruct(構造体)または単純なclassです。

  • Viewは、ビジュアル要素と画面上のコントロールを表示します。 通常はUIViewのサブクラスです。

  • ViewModelは、モデル情報をviewに表示できる値に変換します。 通常はクラスなので、参照として渡すことができます。

このパターンは有名でしょうか? はい、Model-View-Controller(MVC)とよく似ている。 このページの上部にあるクラス図には、ViewControllerが含まれています。 ViewControllerはMVVMに存在しますが、その役割は最小限に抑えられています。

When Should You Use It? (いつMVVMを使うべきか)

viewのために別の表現にモデルを変換する必要がある場合は、このパターンを使用します。たとえば、ViewModelを使用してDateを日付に形式化したStringに、Decimalを通貨に形式化したStringに、または他の多くの便利な変換に変換できます。

このパターンは、特にMVCを補完します。ViewModelがなければ、モデルからビューへの変換コードをViewControllerに書くことになります。ですが、ViewControllerは、viewDidLoadやその他のviewのライフサイクルイベントの処理、IBActionsや他のいくつかのタスクを介したviewのコールバックの処理などで、すでにかなりの作業を行っています。

これは、開発者にとっては冗談の意味ですが "MVC:Massive View Controller"と呼ぶものにつながります。

どのようにしてView Controllerのオーバーサイズを避けることができますか? それは簡単です - MVC以外のパターンを使用してください! MVVMは、いくつかのモデルからビューへの変換を必要とする大規模なViewControllerをスリム化するのに最適な方法です。

Playground Example

開始ディレクトリにIntermediateDesignPatterns.xcworkspaceを開き、MVVMページを開きます。

この例では、ペットを採用するアプリの一部として「Pet View」を作成します。コード例の後に次を追加します。

playground.swift
import PlaygroundSupport
import UIKit

// MARK: - Model
public class Pet {
  public enum Rarity {
    case common
    case uncommon
    case rare
    case veryRare
  }

  public let name: String
  public let birthday: Date
  public let rarity: Rarity
  public let image: UIImage

  public init(name: String,
              birthday: Date,
              rarity: Rarity,
              image: UIImage) {
    self.name = name
    self.birthday = birthday
    self.rarity = rarity
    self.image = image
  }
}

ここでは、Petを命名されたモデルを定義します。 すべてのペットはnamebirthdayrarityimageを持っています。これらのプロパティはビューに表示する必要がありますが、birthdayrarityは直接表示できません。最初にViewModelで変換する必要があります。

次に、あなたのplaygroundの最後に次のコードを追加します:

playground.swift
// MARK: - ViewModel
public class PetViewModel {

  // 1
  private let pet: Pet
  private let calendar: Calendar

  public init(pet: Pet) {
    self.pet = pet
    self.calendar = Calendar(identifier: .gregorian)
  }

  // 2
  public var name: String {
    return pet.name
  }

  public var image: UIImage {
    return pet.image
  }

  // 3
  public var ageText: String {
    let today = calendar.startOfDay(for: Date())
    let birthday = calendar.startOfDay(for: pet.birthday)
    let components = calendar.dateComponents([.year],
                                             from: birthday,
                                             to: today)
    let age = components.year!
    return "\(age) years old"
  }

  // 4
  public var adoptionFeeText: String {
    switch pet.rarity {
    case .common:
      return "$50.00"
    case .uncommon:
      return "$75.00"
    case .rare:
      return "$150.00"
    case .veryRare:
      return "$500.00"
    }
  }
}

上記で行ったことを言いますと、

  1. 最初に、petcalendarという2つのprivateのプロパティを作成し、両方をinit(pet :)で初期化しました。

  2. 次にnameimageの2つの計算されたプロパティを宣言しました。そこではそれぞれペットのnameimageを返します。 これは、値を変更せずに返すのに実行できる最も簡単な変換になります。 すべてのペットの名前にプレフィックスを付けるように設計を変更したい場合は、ここでnameを変更することで簡単に行うことができます。

  3. 次に、別の計算されたプロパティとしてageTextを宣言しました。calendarを使用して、今日の開始からペットのbirthdayまでの年数の差を計算し、これをyears oldに従った形にしてStringとして返します。 他の文字列フォーマットを実行することなく、この値をビューに直接表示することができます。

  4. 最後に、最終的に計算されたプロパティとしてadoptionFeeTextを作成します。このプロパティでは、rarityに基づいてペットの導入コストを決定します。 ここでも、これをStringとして返すと、直接表示することができます。

これでペットの情報を表示するUIViewが必要になりました。 次のコードをplaygroundの最後に追加します。

playground.swift
// MARK: - View
public class PetView: UIView {
  public let imageView: UIImageView
  public let nameLabel: UILabel
  public let ageLabel: UILabel
  public let adoptionFeeLabel: UILabel

  public override init(frame: CGRect) {

    var childFrame = CGRect(x: 0, y: 16,
                            width: frame.width,
                            height: frame.height / 2)
    imageView = UIImageView(frame: childFrame)
    imageView.contentMode = .scaleAspectFit

    childFrame.origin.y += childFrame.height + 16
    childFrame.size.height = 30
    nameLabel = UILabel(frame: childFrame)
    nameLabel.textAlignment = .center

    childFrame.origin.y += childFrame.height
    ageLabel = UILabel(frame: childFrame)
    ageLabel.textAlignment = .center

    childFrame.origin.y += childFrame.height
    adoptionFeeLabel = UILabel(frame: childFrame)
    adoptionFeeLabel.textAlignment = .center

    super.init(frame: frame)

    backgroundColor = .white
    addSubview(imageView)
    addSubview(nameLabel)
    addSubview(ageLabel)
    addSubview(adoptionFeeLabel)
  }

  @available(*, unavailable)
  public required init?(coder: NSCoder) {
    fatalError("init?(coder:) is not supported")
  }
}

ここでは、ペットのimageを表示するimageViewと、ペットのname、age、adoptionFeeを表示する3つのラベルの合計4つのsubviewsを持つPetViewを作成します。 init(frame :)に各ビューを作成して配置します。 最後に、サポートされていないことを示すためにinit?(coder:)の中にfatalErrorを投げます。

あなたはこれらのクラスをactionに移す準備ができました! 次のコードをplaygroundの最後に追加します。

playground.swift
// MARK: - Example
// 1
let birthday = Date(timeIntervalSinceNow: (-2 * 86400 * 366))
let image = UIImage(named: "stuart")!
let stuart = Pet(name: "Stuart",
                 birthday: birthday,
                 rarity: .veryRare,
                 image: image)

// 2
let viewModel = PetViewModel(pet: stuart)

// 3
let frame = CGRect(x: 0, y: 0, width: 300, height: 420)
let view = PetView(frame: frame)

// 4 
view.nameLabel.text = viewModel.name
view.imageView.image = viewModel.image
view.ageLabel.text = viewModel.ageText
view.adoptionFeeLabel.text = viewModel.adoptionFeeText

// 5
PlaygroundPage.current.liveView = view

ここにしたことは

  1. まず、stuartと命名した新しいPetを作成しました。
  2. 次に、stuartを使用してviewModelを作成しました。
  3. 次に、iOSに共通のframeサイズを渡してviewを作成しました。
  4. 次に、viewModelを使用してviewのサブビューを設定しました。
  5. 最後に、viewPlaygroundPage.current.liveViewに設定します。これは、標準のAssistant editorの内でレンダリングするようにplaygroundに指示します。

この動作を確認するには、ViewAssistant EditorShow Assistant Editorを選択して、レンダリングされたビューをチェックアウトします。

Stuartはどんな型のペットでしょうか?彼はもちろん、cookie monsterです! 彼らは非常にまれです。

この例には1つの最終的な改善点があります。 PetViewModelにクラスの中括弧の直後に次のextensionを追加します。

playground.swift
extension PetViewModel {
  public func configure(_ view: PetView) {
    view.nameLabel.text = name
    view.imageView.image = image
    view.ageLabel.text = ageText
    view.adoptionFeeLabel.text = adoptionFeeText
  }
}

この方法を使用して、このインラインではなくViewModelを使用してビューを構成します。

以前入力した次のコードを探します。

playground.swift
// 4 
view.nameLabel.text = viewModel.name
view.imageView.image = viewModel.image
view.ageLabel.text = viewModel.ageText
view.adoptionFeeLabel.text = viewModel.adoptionFeeText

そのコードを次のコードに置き換えます。

playgrond.swift
viewModel.configure(view)

これは、すべてのビュー構成のロジックをViewModelに入れるうえで便利な方法です。 あなたは実際にこれをしたくなかったりしたかったりするかもしれません。 1つのviewでViewModelのみを使用している場合は、ViewModelにconfigureメソッドを配置するとよいでしょう。 ただし、複数のviewでViewModelを使用している場合は、ViewModel内のすべてのロジックを入れておくと、そのモデルを混乱させる可能性があります。 この場合、各viewごとに構成コードを切り離す方が簡単です。

あなたの出力は前と同じになるはずです。

What Should You Be Careful About? (気をつけるべき点は?)

MVVMは、アプリで多くのModelからViewへの変換が必要な場合に適しています。 しかし、すべてのオブジェクトがModel、ViewまたはViewModelのカテゴリにきちんと適合するわけではありません。 代わりに、他のデザインパターンと組み合わせてMVVMを使用する必要があります。

さらに、MVVMはアプリケーションを初めて作成するときにあまり役に立ちません。MVCは良い出発点かもしれません。アプリの要件が変わると、変化する仕様の要件に基づいて異なるデザインパターンを選択する必要があるでしょう。本当に必要なときに、後でアプリのlifetimeにMVVMを導入することは大丈夫です。変化を恐れてはいけません。代わりにそれを先取りして計画してください。

あとがき (翻訳部分ではなく筆者執筆)

この後にチュートリアルに関する説明がありますがそこはMVVMの本質に関わる部分ではありませんので個々人で取り組むと理解が深まると思います。巷では、と言いますかiOSやAndroidのモバイルアプリケーション開発でMVVMが凄く流行っています。ちなみに私はMVVMは特に否定派ではありませんが好き好んでMVVMを導入したくありません。どっちかといえばMVC派です。
だって、例えば個人の趣味アプリを開発するときのことを考えてみてください。
もしあなたがお金を稼ぎたくなって個人の趣味アプリを開発する場合わざわざこのクラスはこの役割にして実装して、これはProductクラスのViewModelに関わるところだからViewModelチックに設計して・・・なんてやってたらいつまでたってもアプリ開発が進みませんし(笑)。
即効性(即効でアプリをリリースするところまで開発するという意味)を考えたらMVCの方が早いです。
仕事でのアプリ開発と個人の趣味アプリなんて土俵が違うだろ、って思うかもしれません。
このように考えてる限りで個人開発で稼ぐアプリを作ることは既に無理感が出ていますけど。
なので、巨大なアプリに発展してきたら開発を楽するためにMVVMを導入するスタンスになるのがほとんどな気がします。
なので近年で取り上げられてるMVVMってめっちゃイケてるぜ感は持ち上げられすぎている気がするのが私の意見です。

ちなみに私にとって興味があるのは

MVCとMVVMだとどっちの方がバグが少なくなるか!

というところ。
MVVMで開発しているアプリもジョインしたことありますが、CrashlyticsでのバグレポートはMVCで開発しているアプリとそんなかわらない感がありました。
というより、MVVMを導入しているアプリはRxSwiftを導入しているアプリが多いのもあってRxSwiftのライブラリの根本部分で起きていたバグが多々見られました(笑ではなく)
(さらにもっと言えば、そういうバグはRxSwiftやMVVMを導入した人が責任を持って改修すれば問題にならないのですがそういうバグに限って導入者が改修しないパターンが多い気がします(いわゆるやり逃げ))。

その辺、どうなんですかね。

そういう悲劇を見たくない意味も込めて私は断然MVC派です。