LoginSignup
13

More than 3 years have passed since last update.

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

Posted at

はじめに

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

そもそもCocoa MVCとは

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

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

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

スクリーンショット 2020-12-21 20.31.55.png

ViewControllerがおFatになるワケ

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

UIViewControllerでUIも制御するべきであるという認識

私もそうだったのですが、多くの人がやりがちなのが、責務を分けずにUIViewControllerのクラスにViewControllerの両方の責務をのせてしまっている事が多いイメージです。
では、何故ViewControllerの責務を分けずに1クラスで実装してしまうのでしょうか?
そもそもの発端はUIViewControllerにあります。

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

スクリーンショット 2020-12-21 20.55.46.png

Cocoa MVCにおいてのViewControllerの役割

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

Cocoa MVCを実装してみる

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

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)
        }
    }
}

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

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

参考文献

Model-View-Controller
PEAKS(ピークス)|iOSアプリ設計パターン入門

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
13