mvc
Swift

デザインパターン入門「Model-View-Controller」

最近、デザインパターンが増えてきたので、まとめていこうかなと思います。

記念すべき第1回はMVCです!!

MVCを調べてたら、何種類か出てきて困りましたが、自分が理解できて使えそうな形のものを紹介していきたいと思います。
なので、自分の知ってるのと違うという場合もありますが、コメントなどで教えてもらえたら勉強になりますのでぜひお願いします:angel_tone2:(笑)

今回の実装コードは、Swiftが個人的に好きなのでSwiftをつかいます:thumbsup:

MVC構成要素の概要

Model
データの保持
データの管理
ビジネスロジック
View
modelの情報を表示する
Controller
ユーザーからの入力を受け取る
modelへ入力を伝える

iOSではViewControllerというクラスがControllerの役割をします
今回は、ViewControllerクラスにViewも持たせました

MVC構成要素の実装

Model

MVCModel.swift
import Foundation

// データの変更を通知するためにdelegateを実装
protocol MVCModelDelegate {
    func didChange()
}

class MVCModel {

    public var count = 0;
    var delegate: MVCModelDelegate? = nil

    // ④データの変更用メソッドを定義
    func inc() {
        count += 1
        delegate?.didChange()
    }

    func dec() {
        count -= 1
        delegate?.didChange()
    }

}

ViewController

MVCViewController.swift
import Foundation
import UIKit

class MVCViewController: UIViewController, MVCModelDelegate {

    /** Controller側の実装 **/

    let mvcModel = MVCModel()

    override func viewDidLoad() {
        super.viewDidLoad()
        // 初期値を表示するためにview側の表示更新を呼ぶ
        didChange()
        mvcModel.delegate = self
    }

    // ②ユーザーからの入力イベント
    @IBAction func inc(_ sender: Any) {
        // ③Modelのメソッドを呼ぶ
        mvcModel.inc()
    }

    @IBAction func dec(_ sender: Any) {
        mvcModel.dec()
    }

    /** View側の実装 **/

    @IBOutlet weak var countLabel: UILabel!

    // ⑤Modelからのdelegate
    func didChange() {
        countLabel.text = String(mvcModel.count)
    }

}

制御フロー

  1. ユーザーが入力をする(incボタンをタップ)
  2. Controllerが入力を受け取る
  3. 入力に対応したModelのメソッドを呼ぶ
  4. Modelのデータが変更された場合は、delegateが呼ばれる
  5. Viewで表示を更新する

画面遷移

画面遷移がある場合の実装

  • 遷移元(MVCViewController.swift
  • 遷移先(MVCSubViewController.swift

ViewController

MVCViewController.swift
extension MVCViewController {

    // ②ユーザーからの入力イベント
    @IBAction func display(_ sender: Any) {
        // SubViewController画面に遷移
        performSegue(withIdentifier: "presentMVCSubViewController", sender: self)
    }

    // ③画面遷移の際呼ばれる
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        // ④遷移先のViewControllerにModelクラスを渡す
        if let destination = segue.destination as? MVCSubViewController {
            destination.mvcModel = mvcModel
        }
    }

}

SubViewController

MVCSubViewController.swift
import Foundation
import UIKit

class MVCSubViewController: UIViewController {

    var mvcModel: MVCModel? = nil

    @IBOutlet weak var countLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
        // ⑤初期値を表示
        if let count = mvcModel?.count {
            countLabel.text = String(count)
        }
    }

}

制御フロー

  1. ユーザーが入力をする(画面遷移ボタンをタップ)
  2. Controller(MVCViewController)が入力を受け取る
  3. Controller(MVCSubViewController)画面に遷移する
  4. Modelクラスの参照をMVCSubViewControllerクラスに渡す
  5. View(MVCSubViewController)で渡されたModelのデータを表示する

まとめ

作ってみた感想になってしまいますが、メリットとデメリット:point_up:

メリット

  • 制御フローがUser → Controller → Model → Viewとわかりやすくなっている
  • 「わかりやすい = 作りやすい」と思う
  • Modelだけを渡すので、ViewController間でのデータの受け渡しが簡単

デメリット

  • ViewControllerが含まれるのでViewControllerクラスが肥大化する
  • Modelもデータの種類ごとにうまく分けないと肥大化する
  • Modelが複数個できたときに、どのControllerで参照されているのかを追うのが大変

追記

2017.11.16

お互いに強参照していたため、メモリリークを起こす可能性があるとの指摘をいただきましたので修正をこちらに書かせていただきます。

MVCModel.swift
// データの変更を通知するためにdelegateを実装
protocol MVCModelDelegate: class { // ← class制約
    func didChange()
}

class MVCModel {

    public var count = 0;
    weak var delegate: MVCModelDelegate? = nil // ← 弱参照へ