Cocoa MVCがおFatになりやすいワケ

## はじめに
iOSアプリ開発をする上で切っても切り離せない問題として`FatViewController`が挙げられます。
そんな中でも、`Cocoa MVC`は`FatViewController`になりやすいという印象を皆さんもお持ちではないでしょうか？
`MVC`とは`Model` `View` `Controller`の略ですが、`ViewController`が肥大化しやすいことから、`MassiveViewController`と呼称されてしまうことがある程です。
では、なぜ`Cocoa MVC`はおFatになりやすいのでしょうか？
それを今回私なりに調べてみました。

## そもそもCocoa MVCとは
`Cocoa MVC`とは、`Model` `View` `Controller`の3つに役割を分割するアーキテクチャです。

|名称|役割|
|:--|:--|
|Model|データの保持、及び処理を行い、状態の更新を通知します。|
|View|画面の描画処理を行います。|
|Controller|ユーザからの処理を受け付け、それを元にModelへに処理を依頼、Modelが変更されたのを検知し、Viewの描画更新処理を行います。|

`Controller`は`View`と`Model`の2つの参照を持ちます。
`Controller`は`View`から受け取ったユーザーアクション(ボタンタップ等)を元に、`Model`に更新処理を依頼し、`Model`は依頼を元にデータを更新します。
`Controller`は`Model`のデータが更新されたことを検知し、検知した内容を元に`View`の描画の更新を行います。
`Controller`が`View`と`Model`の橋渡し役をするため、`View`と`Model`はお互いに参照し合うことはありません。
※ 補足になりますが、上記の`Controller`のようにオブジェクト同士が参照しないように仲介役を用意し、オブジェクト同士の結合度を弱めるパターンを**Mediatorパターン**と言います。

![スクリーンショット 2020-12-21 20.31.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556106/5e7ced94-fc3f-ff8a-4bd9-29ca87251995.png)

## ViewControllerがおFatになるワケ
`Cocoa MVC`の概要を説明しましたが、その上で何故`Cocoa MVC`は`MassiveViewController`と言われるほど`ViewController`がおFatになりやすいのでしょうか？
次は`FatViewController`になりやすい要因となっている部分について見ていきましょう。

### UIViewControllerでUIも制御するべきであるという認識
私もそうだったのですが、多くの人がやりがちなのが、責務を分けずに`UIViewController`のクラスに`View`と`Controller`の両方の責務をのせてしまっている事が多いイメージです。
では、何故`View`と`Controller`の責務を分けずに1クラスで実装してしまうのでしょうか？
そもそもの発端は`UIViewController`にあります。

`UIViewController`というのは`UIKit`つまり、UIを制御するためのコンポーネントとして用意されているものです。
なので、`UIViewController`は画面のライフサイクルを持ち、`Storyboard`と接続することで`View`の描画を行います。
ここから、私たちは`UIViewController`に`Controller`としての責務だけではなく、`View`としての責務を載せてしまいがちなのです。
そうした結果、生まれるのが我らが`FatViewController`になります。

![スクリーンショット 2020-12-21 20.55.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/556106/82986c37-1ced-2e2d-9d2a-e8d9f395f16e.png)

## Cocoa MVCにおいてのViewControllerの役割
`Cocoa MVC`が`FatViewController`になりやすい理由は分かりました。
では、どうすればおFatにならないのか？
その答えは至極簡単です。
`Cocoa MVC`における`UIViewController`の役割はあくまで`Controller`として`View`と`Model`の橋渡し、及び制御に、UIの制御は`UIView`に専念させることで、`UIViewController`に`View`の役割を持たせないようにさせるのです。
`UIViewController`でUIを制御するという誘惑に負けず、きっちりしっかりと責務を分けましょう。
それがおFatから抜け出す第一歩です。

### Cocoa MVCを実装してみる
では、実際にコードを通して`ViewController`に`Controller`の責務だけを持たせた場合の`Cocoa MVC`の記述例を見ていきたいと思います。
※ 通信処理など、今回の大筋から逸れる部分については記載しておりません。

```swift:ArticlesViewController.swift
import UIKit

/// 記事一覧表示用Controller
final class ArticlesViewController: UIViewController {

    // MARK: - Variables

    /// 表示するView
    private lazy var articleView = ArticlesView()

    /// 表示に使用するモデル
    private var model: ArticlesModel? {
        didSet {
            registerModel()
        }
    }

    /// 記事データ
    private var articles: [Article] = [] {
        didSet {
            articleView.articlesTableView.reloadData()
        }
    }

    // MARK: - Lifecycle Methods

    override func loadView() {
        super.loadView()
        view = articleView
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        model = ArticlesModel()
        model?.request()
    }
}

// MARK: - Private Methods
private extension ArticlesViewController {
    /// Model登録時の処理(ModelとViewの結合、及びModelの監視を開始)
    func registerModel() {
        model?.delegate = self
        articleView.articlesTableView.dataSource = self
        articleView.articlesTableView.delegate = self
    }
}

// MARK: - UITableViewDataSource
extension ArticlesViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return articles.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        // 記事一覧用のCellを構築するための処理(今回の記事の内容からは話が逸れるため、処理の詳細は省略)
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "ArticleCell", for: indexPath) as? ArticleCell else {
            return UITableViewCell()
        }
        let article = articles[indexPath.row]
        cell.configure(with: article)
        return cell
    }
}

// MARK: - UITableViewDelegate
extension ArticlesViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        // タップ時の処理を記載
    }
}

// MARK: - ArticlesModelDelegate
extension ArticlesViewController: ArticlesModelDelegate {
    func completedRequest(result: Result<[Article], Error>) {
        switch result {
        case .success(let value):
            articles += value
        case .failure(let error):
            print(error)
        }
    }
}

```

```swift:ArticlesView.swift
import UIKit

/// 記事一覧View
final class ArticlesView: UIView {

    // MARK: - Constants

    /// identifer
    private static let identifer = "ArticlesView"

    // MARK: - Outlets

    /// 記事一覧表示用TableView
    @IBOutlet private(set) weak var articlesTableView: UITableView! {
        didSet {
            let identifer = type(of: self).identifer
            articlesTableView.register(UINib(nibName: identifer, bundle: nil), forCellReuseIdentifier: identifer)
        }
    }

    // MARK: - Lifecycle Methods

    override init(frame: CGRect) {
        super.init(frame: frame)
        initialze()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        initialze()
    }
}

// MARK: - Private Methods
private extension ArticlesView {
    /// 初期化する
    func initialze() {
        guard let view = Bundle.main.loadNibNamed(type(of: self).identifer,
                                                  owner: self,
                                                  options: nil)?.first as? UIView else {
            return
        }
        view.frame = self.bounds
        self.addSubview(view)
    }
}
```

```swift:ArticlesModel.swift
import Foundation

/// ArticlesModelDelegate
protocol ArticlesModelDelegate: class {
    /// 記事リクエスト完了時に通知
    /// - Parameter result: リクエスト結果
    func completedRequest(result: Result<[Article], Error>)
}

/// 記事一覧Model
final class ArticlesModel: NSObject {

    // MARK: - Variables

    /// ArticlesModelDelegate
    weak var delegate: ArticlesModelDelegate?

    /// 記事
    private(set) var articles: [Article] = []

    /// 記事をリクエスト
    func request() {
        // 記事一覧をリクエストする処理(今回の記事の内容からは話が逸れるため、処理の詳細は省略)
        ApiClient.request(completion: { [weak self] result in
            self?.delegate?.completedRequest(result: result)
        })
    }
}

```

ざっくりと記述してみましたがいかがでしょうか？
少なくとも、`ViewController`には`Controller`としての役割だけで、画面描画に関する処理は無くなり、多少は見通しが良くなったのではないでしょうか？
上記のようにきちんと責務を分けることで、おFatな`ViewController`を回避することができるかもしれません。

## おわりに
今回は`Cocoa MVC`が`FatViewController`になりやすい理由について私なりの考えをまとめさせていただきました。
もちろん、`FatViewController`にならないようにするための対策方法は様々です。
`Cocoa MVC`で肥大化するのであれば、別のアーキテクチャを使用するのももちろん一つの手です。
必要に応じて適切なアーキテクチャを選択し、各クラスが持つべき責務をしっかりと考えて実装していくことで、`FatViewContoller`を撲滅していきましょう！

また、本記事はあくまで私の独断と偏見になりますので、もしご意見や改善案等ある方いらっしゃいましたら、コメントを頂けますと大変幸いです。
ご一読いただきありがとうございました！

## 参考文献
[Model-View-Controller](https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.html#//apple_ref/doc/uid/TP40008195-CH32-SW1)
[PEAKS（ピークス）｜iOSアプリ設計パターン入門](https://peaks.cc/books/iOS_architecture)