Massive View Controllerの真実


Massive View Controller。おそらこの言葉を聞いたことがある人は、それに悩まされたことがある人かもしれない。Massive View ControllerとはView Controllerに処理が集中して一つのView Controllerの膨大の量のコードが書かれてしまったもののことを言う。

View ControllerがMassiveになってしまうと

  • コードの見通しが悪い
  • コードを変更しにくい
  • テストがしにくい
  • etc.

などなど様々な弊害が発生する。もっと色々あると思うが、それはMassive View Controllerと対峙してきた各々の心の中にとどめておいてもらおう。ではなぜView ControllerはMassiveになってしまうのか?その原因を探り、View Controllerのダイエット方法の見つける参考にしたい。

MVCは実はMとVだけ

Massive View ControllerはMVCのアーキテクチャで生まれることが多い。それはMVCではViewControllerが事実上のViewであると言うことに起因する。

ViewControllerは画面のライフサイクルメソッドを持っいたり、Storyboardに配置されたViewと不可分であったりする。そして何よりUIViewControllerはUIKitのコンポーネントであり、UIViewControllerはControllerではなく完全にUIとして作られたものである。

そのためMVCではView(とController), Modelという2つの層だけしか存在しないことになってしまう。そして、本来Viewとは独立した場所に書かれる様々なコードが全てViewControllerに書かれてしまう。


Realistic Cocoa MVC from iOS Architecture Patterns by Bohdan Orlov

これがMassive View Controllerの正体である。

以下の例のActionやNavigationの部分に注目してもらいたい。
この部分のコードは全てViewからは完全に切り離されているべき部分である。

MyViewController.swift
import UIKit

class MyViewController: UIViewController, UITextFieldDelegate {

    // Properies
    var users: [User] = [] // Viewがmodelを保持してしまっている
    let api = UserApi() // Viewが通信ロジックを持ってしまっている



    // Views

    lazy var addUserButton: UIButton = {
        let b = UIButton(type: .system)
        b.addTarget(self, action: #selector(addUserButtonTap), for: .touchUpInside)
        return b
    }()

    lazy var sendEmailButton: UIButton = {
        let b = UIButton(type: .system)
        b.addTarget(self, action: #selector(sendEmailButtonTap), for: .touchUpInside)
        return b
    }()

    lazy var userFilterTextField: UITextField = {
        let tf = UITextField()
        tf.delegate = self
        return tf
    }()



    // Lifecycle

    override func viewDidLoad() {
        super.viewDidLoad()
        loadUsers()
    }



    // Event

    @objc func addUserButtonTap() {
        // ここで呼ぶ処理はViewの外で行うべきもの
        pushAddUserViewController()
    }

    @objc func sendEmailButtonTap() {
        // ここで呼ぶ処理はViewの外で行うべきもの
        sendEmail()
    }

    func textFieldDidEndEditing(_ textField: UITextField) {
        // ここで呼ぶ処理はViewの外で行うべきもの
        filterUsers(by: textField.text!)
    }



    // Action: 本来のViewの責務ではない

    func loadUsers() {
        api.fetchUsers { result in
            switch result {
            case .success(let users):
                self.users = users
            case .failure(let error):
                break
            }
        }
    }

    func sendEmail() {
        /*
         外部にEmailを送る処理
         */
    }

    func filterUsers(by text: String) {
        users = users.filter { $0.name == text }
    }



    // Navigation: 本来のViewの責務ではない

    func pushAddUserViewController() {
        let vc = AddUserViewController()
        navigationController?.pushViewController(vc, animated: true)
    }
}

MVCはレイヤー数がまさかの1つ?

さてさらにもう一つ衝撃の事実がある。ViewとControllerが一緒が一つになっていたとしても、Model層が残されている。となるとMVCは2層構造となるはずだ。

しかし一般的なiOSアプリでModelの位置付けを考えてみるとそうでもないことがわかる。iOSアプリは多くの場合はModelを通信を行い外部サーバー等から取ってくる。本質的なビジネスロジックはサーバーにおいてあるので、iOS側ではModelはただの情報がはいった入れ物にすぎない。そこには全くロジックが書かれていないことが多い。

こうなるとプロジェクトに書くコードのほぼ全てが事実上のViewであるView Controllerに書かれてしまう。すなわちMVCではレイヤーが1つしかないのも同然ということになってしまう。

あらゆる処理がView Controllerへ書かれていればView Controllerが巨大になってしまうのも必然かもしれない。

解決案

Massive View ControllerからViewとしての機能以外を全て他の部分へ移動させることでView Controllerをスリムにできる。具体的には以下のようなアーキテクチャで実現できる。

  • MVVM
  • MVP
  • VIPER(Clean Architecture)

そしてどのようなアーキテクチャであれ、View Controllerはおおよそ以下のように変わる。

SlimViewController.swift
import UIKit

class SlimViewController: UIViewController, UITextFieldDelegate {

    // Views: Propertyとして保持するのはViewのみ

    lazy var addUserButton: UIButton = {
        let b = UIButton(type: .system)
        b.addTarget(self, action: #selector(addUserButtonTap), for: .touchUpInside)
        return b
    }()

    lazy var sendEmailButton: UIButton = {
        let b = UIButton(type: .system)
        b.addTarget(self, action: #selector(sendEmailButtonTap), for: .touchUpInside)
        return b
    }()

    lazy var userFilterTextField: UITextField = {
        let tf = UITextField()
        tf.delegate = self
        return tf
    }()



    // Lifecycle

    override func viewDidLoad() {
        super.viewDidLoad()

        /*
         userのロードはviewの外で行う
         */
    }




    // Event: アーキテクチャによってはeventの受け取りもviewの外部で行なっても良い∂

    @objc func addUserButtonTap() {
        /*
         viewの外へ何か指示
         */
    }

    @objc func sendEmailButtonTap() {
        /*
         viewの外へ何か指示
         */
    }

    func textFieldDidEndEditing(_ textField: UITextField) {
        /*
         viewの外へ何か指示
         */
    }

}

多くのロジックが消えて、ピュアはViewとなっているのがわかると思う。

MVCは悪か?

MVCを全否定したいわけではない。MVCでもViewControllerの中でしっかりコードを整理すればコードの見通しはそれなりに保てるし、何より開発初期であれば高速に開発が行える。またViewをViewControllerから分離して別に宣言して、ViewControllerからViewの機能を切り離すことを試みたりもできる。

ただView Controllerのテストが書きにくいと言う問題への対処は難しいかもしれない。

まとめ

Massive View ControllerはMVCのアーキテクチャで起こりやすい。それはView Controllerが事実上のViewとなっているのに、ViewControllerへあらゆる処理を書かなけれいけなくなってしまうことに起因する。

MVVC, MVP, VIPER(Clean Architecture)等のアーキテクチャを導入してレイヤーを増やすことで、View Controllerにあったビジネスロジック等を別の場所へ移動することで、View Controllerをダイエットさせるこができる。どのような方法であれ最終的にダイエットしたView ControllerはピュアなViewとしての機能だけが残る。

またMVCでも綺麗に整理されたコードを書くことは可能なので、アーキテクチャの選定は状況に応じて柔軟に行うべき。