はじめに
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パターンと言います。
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になります。
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の記述例を見ていきたいと思います。
※ 通信処理など、今回の大筋から逸れる部分については記載しておりません。
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)
}
}
}
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)
}
}
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を撲滅していきましょう!
また、本記事はあくまで私の独断と偏見になりますので、もしご意見や改善案等ある方いらっしゃいましたら、コメントを頂けますと大変幸いです。
ご一読いただきありがとうございました!

