はじめに
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
を撲滅していきましょう!
また、本記事はあくまで私の独断と偏見になりますので、もしご意見や改善案等ある方いらっしゃいましたら、コメントを頂けますと大変幸いです。
ご一読いただきありがとうございました!