20
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

手を動かして理解するCocoa MVCパターン

Last updated at Posted at 2020-06-29

cover

はじめに

本記事ではiOSアプリ実装では最もよく用いられているであろうCocoa MVCパターンについて解説します。
巷ではクリーンアーキテクチャーはじめ様々なソフトウェアアーキテクチャーが出てきていますが、Swiftでアプリケーションを構築する上で余程大規模にならない限りは、Apple自体が推していることもあり、このCocoa MVCパターンを使うのが自分の経験上最もいいのではないかなと思います。

ソフトウェアアーキテクチャーは本などで読むとふむふむなるほどと思うのですが、いざ実装しようとしてみると難しいことが多いです。
そこで今回は実際に非常に単純なアプリケーションを実装しながら手を動かして理解するような構成にしています。
では早速始めていきましょう!

Cococa MVCパターンについてのイメージを掴む

何も知らない状態で手を動かしてもあまり学習効率が良くないので、先にCocoa MVCのイメージを掴んでおきましょう。
実際手を動かした後もう一度復習するので、この時点ではこんなもんだなーくらいの理解で構いません。

MVCパターンとはModel-View-Controller Patternの略で、その名の通りアプリケーション内のコードをModel、View、Controllerの3つの役割に分割します。

Model

Modelはデータの保持及び処理を担当します。
具体的には通信処理や計算処理、ローカルストレージに対する保存処理など、後述のView、Controller以外全てがModelに含まれます。RailsなどのWebアプリのフレームワークにおけるSQLデータベース上のテーブルを表現するModelとは意味が異なるので注意が必要です。
またModelは保持しているデータが変更されたら、そのModelを購読しているControllerに変更を通知します。

View

Viewは画面の描画処理を行います。
再利用性を高めるためにも極力ロジックは含めずに、入力を受けてそれをそのまま描画するような設計にすることが重要です。

Controller

ControllerはModelとViewの参照を保持し、Modelについては変更を監視します。
Controllerはユーザーの入力を受けつけ、Modelに処理を依頼し、Modelが変更されたのを検知してViewの描画を更新します。

これらをまとめると以下のようになります。

名前 役割
Model データ保持、処理を行う
View 画面描画を行う
Controller ユーザーの入力を受け付ける。ModelとControllerの橋渡し役となりアプリ全体をコントロールする。

グループ 40.png
ここで非常に重要なポイントはControllerがViewとModelの橋渡し役となることで、ViewとModelが何かに依存せずに完全に独立しているところです。
こうすることで、ViewとModelは様々なところで再利用することができるようになります。
逆にControllerは特定のViewとModelに強く依存しており、Cocoa MVCは
Controllerの再利用性を犠牲にViewとModelの再利用性を極限まで高めた設計と言えます。

そして実際にユーザーの入力〜画面の描画までの全体の処理の流れは以下のようになります。

グループ 43.png

  1. [Controller] ユーザーの入力を受け付ける。
  2. [Controller] Modelに処理を依頼する。
  3. [Model] データを処理する。
  4. [Model] データの処理結果を購読しているControllerに通知する
  5. [Controller] Modelの変更を検知する。
  6. [Controller] Viewに描画を指示する。
  7. [View] 画面に描画を行う。

ではここままででざっくりイメージがつかめたと思うので、次項から実際に手を動かして実装していきましょう!

手を動かして実装する

今回作成するアプリは+ボタン、-ボタンで数字を増減させることができるシンプルなカウンターアプリです。

ezgif.com-crop.gif

こちらをCocoa MVCパターンを使って実装していきましょう。
ゼロからプロジェクト作成をしていただいても構いませんし、初期の状態と完成形をこちらに用意しておいたので、こちらをCloneしてJP/Starterから始めても構いません。
https://github.com/kazuooooo/CocoaMVCFromScratch

CounterModelを実装する

まずはMVCのM、ModelにあたるCounterModelを実装していきましょう。
前述の通りデータの保持、処理を行うのがModelの役割なので、

  • 今数値がいくらなのか保持する(データの保持)
  • 数字を増やす/減らす(データの処理)
  • Modelを監視しているコントローラーに変更を通知する

が必要です。

CounterModel.swift
import Foundation
class CounterModel {
    static let notificationName = "CounterModelChanged"
    
    let notificationCenter = NotificationCenter()
    // 今数値がいくらなのか保持する(データの保持)
    internal var count: Int = 0 {
        didSet {
            // Modelを監視しているコントローラーに変更を通知する
            notificationCenter.post(
                name: .init(rawValue: CounterModel.notificationName),
                object: count
            )
        }
    }
    // 今数値がいくらなのか保持する(データの保持)
    func countUp(){ count += 1 }
    func countDown(){ count -= 1 }
}

CounterViewを実装する

続いてMVCのVにあたるCounterViewを実装します。
Viewの役割は描画処理を行うことです。
CountViewはrenderというメソッドを通してcountLabelに描画処理を行います。

CounterView.swift
import Foundation
import UIKit

class CounterView: UIView {
    
    @IBOutlet weak var countLabel: UILabel!
    public func render(count: Int){
        countLabel.text = String(count)
    }
}

続いて実際の画面をStoryBoardで作成してください。(Starterを使っている場合は事前に作成してあります。)
Main_storyboard.png

カウントのLabelはIBOutletで接続し、親のViewにCounterViewを設定します。
Main_storyboard.png
Main_storyboard.png

CounterViewControllerを実装する

最後にMVCのC、CounterViewControllerを実装します。

ControllerではViewとModelの橋渡しをするために

  • Modelの変更を監視する
  • ユーザーの入力を受け付けて、Modelに処理を依頼する
  • Modelの変更を検知して、Viewに描画処理を依頼する

を行う必要があります。
実装は以下のようになります。

CounterViewController
import UIKit

class CounterViewController: UIViewController {
    // ViewとModelの参照を保持する
    @IBOutlet var counterView: CounterView!
    private(set) lazy var counterModel = CounterModel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Modelの変更を監視する
        counterModel.notificationCenter.addObserver(
            self,
            selector: #selector(self.handleCountChange(_:)),
            name: .init(NSNotification.Name(rawValue: CounterModel.notificationName)), object: nil
        )
    }
    
    // 変更を検知する
    @objc func handleCountChange(_ notification: Notification) {
        if let count = notification.object as? Int {
            // Viewに描画処理を依頼する
            counterView.render(count: count)
        }
    }
    
    // 入力を受け付ける
    @IBAction func OnPlusButtonTapped(_ sender: Any) {
        // Modelに処理を依頼する
        counterModel.countUp()
    }
    
    @IBAction func OnMinusButtonTapped(_ sender: Any) {
        counterModel.countDown()
    }
}

こちらもボタンのIBActionへの紐付け、ViewControllerクラスの設定を忘れないようにしましょう。
Main_storyboard.png
Main_storyboard.png

さて以上で実装は完了です。
一度Runをして+/-ボタンが正しく動くか確認してみてください!

再度パターンに当てはめて考えてみる

実装は完了しましたが、写経しただけ感があってまだどこかしっくりきていませんよね?
最後にもう一度全体像をコードを見ながら確認していきましょう。
そうすることで理解がグッと深まるはずです。

依存関係を見てみる

まずは依存関係について先ほど見たこちらの図を見ながらコードをもう一度確認してみましょう。

グループ 40.png

Controllerのコードを見てみると、ViewとModelの参照及び、変更の監視がされていることがわかります。

CounterViewController
class CounterViewController: UIViewController {
    // ViewとModelの参照を保持する
    @IBOutlet var counterView: CounterView!
    private(set) lazy var counterModel = CounterModel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Modelの変更を監視する
        counterModel.notificationCenter.addObserver(
            self,
            selector: #selector(self.handleCountChange(_:)),
            name: .init(NSNotification.Name(rawValue: CounterModel.notificationName)), object: nil
        )
    }
...
}

またViewとModelはControllerは処理を受け付けるだけで完全に独立しており、再利用可能なこともコードを見てチェックしておいてください。

処理の流れをチェックする

最後に+ボタンを押した時を例に全体の処理の流れも図とコードを見ながらチェックしていきます。

1.入力を受け付ける

1.png
まずはControllerが+ボタンの入力を受け付けます。

CounterViewController
// 入力を受け付ける
@IBAction func OnPlusButtonTapped(_ sender: Any) {
    ...
}

2. Modelに処理を依頼する

2.png
入力を受けたらControllerはModelに処理を依頼します。

CounterViewController
@IBAction func OnPlusButtonTapped(_ sender: Any) {
    // 2. Modelに処理を依頼する
    counterModel.countUp()
}

3. データを処理する

3.png

Modelはデータを処理します。
今回の場合はcountUpが呼び出されているので自身のcountをインクリメントします。

CounterModel
class CounterModel {
    ...
    // データを処理する
    func countUp(){ count += 1 }
}

4. 変更を通知する

4.png

ModelはNotificationCenterを通じて購読者に変更を通知します。

CounterModel
class CounterModel {
    ...
    internal var count: Int = 0 {
        didSet {
            // Modelを監視しているコントローラーに変更を通知する
            notificationCenter.post(
                name: .init(rawValue: CounterModel.notificationName),
                object: count
            )
        }
    }
    ...
}

5. Modelの変更を検知する

5.png

ControllerはModelのNotificationCenterに登録しておいたObserverからModelの変更を検知します。

class CounterViewController: UIViewController {
    override func viewDidLoad() {
        ...
        // Modelの変更を監視する
        counterModel.notificationCenter.addObserver(
            self,
            selector: #selector(self.handleCountChange(_:)),
            name: .init(NSNotification.Name(rawValue: CounterModel.notificationName)), object: nil
        )
    }
    ...
    
    // 変更を検知する
    @objc func handleCountChange(_ notification: Notification) {
        ...
    }
}

6.描画指示

6.png
ControllerはViewに描画を指示します。

@objc func handleCountChange(_ notification: Notification) {
    if let _ = notification.object as? Int {
        // Viewに描画処理を依頼する
        counterView.render(count: counterModel.count)
    }
}

7.描画処理を行う

7.png

Controllerからの描画指示を受けてViewは描画処理を行います。

CounterView
class CounterView: UIView {
    
    // 描画処理を行う
    public func render(count: Int){
        countLabel.text = String(count)
    }
}

終わりに

いかがだったでしょうか?
ソフトウェアアーキテクチャーパターンは初めはとっつきにくいですが、一度理解してしまえばエンジニアにとって非常に強力な武器になってくれます。
今回のCocoaMVCのようにきっちり理解できていなくて雰囲気で使っているなーというところは一度腰を据えて自分で手を動かしてみることで理解が一気に深まるのでオススメです!

[参考]
iOSアプリ設計パターン入門
Model-View-Controller

20
15
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?