LoginSignup
22
22

More than 5 years have passed since last update.

iosのControllerを分割しよう(MVVMなどのアーキテクチャの話ではないよ)

Last updated at Posted at 2016-05-16

概要

ios開発でどうしてもFat Controllerになってしまう場合の一つの対処方法です。
よく、MVCSS,MVVM,DDDなどで責務を分けましょうという話は聞くと思います。
で、それらのアーキテクチャでControllerにロジックを書かないようにするというのはよく聞く話でしょう。
今回はそういう話でありません。

そもそも論としてControllerの役割が元々多すぎるから、そこを分割しましょう。という話です。
以下がControllerの役割だと思います。

  • 画面遷移
  • 画面のライフサイクル
  • イベントの処理
  • Viewの管理

画面遷移に関しては、ios8対応が必要なくなった時には完全にStoryBoard Referenceがその役割を持つようになるでしょう。
が、まだまだ対応するし、StoryBoardもUIViewControllerの持ち物なので素直にControllerに残します。
「画面のライフサイクル」もどうしてもUIViewControllerを継承していないと実現できないので残します。
残りの「イベントの処理」の大半と「Viewの管理」はUIViewControllerを継承していなくてもできることなので、そこを抜き出しましょう。

やり方

インターフェイスビルダー

ライブラリペインにあるObjectを使うだけです。

Object Template.png

  • Interface BuilderでViewControlerをおきます。
  • 部品のObject を 以下の画像のように配置します。

ObjectOutlet.png

  • Objectを継承したクラス(この記事ではPresenterと呼びます)を作成して、それをCustom classに記述します。

customClass.png

  • いつも通りにStoryboard上のControllerにView部品を配置します。
  • outletでPresenterとViewControllerをつなげます。

Controller-outlet.png

  • 同様にPresenterとView部品をつなげます。

textoutlet.png

これで、ViewControllerにはPresentorの参照だけになり、ViewはPresentorが持つことになります。

コード

以下は、Bondを利用したソースの例です。

Controllerでは、画面のライフサイクルと画面遷移を行います。
Viewは持たずにNSObjectを継承したPresentorを保持します。
Viewへのアクセスが必要な場合は、Presentor経由で呼び出します。

このクラスでは、ほとんどobserveNewを使った画面遷移のコードとライフサイクルに関係している処理を呼び出すだけになると思います。

FirstController.swift
class FirstController: UIViewController {
    @IBOutlet var firstPresentor: FirstPresentor!

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        firstPresentor.setup()
        Weather().transitionState.observeNew { state in
            switch state {
            case .Start:
                self.performSegueWithIdentifier("toSecond", sender: self)
                break
            default:
                break
            }
        }
    }
}

Controllerから分割したクラスをここではPresentorとしています。ポイントはNSObjectを継承することです。
具体的なViewはこのクラスが管理します。
このクラスでは、Bondとの繋ぎこみぐらいが役割になると思います。

FirstPresentor.swift
class FirstPresentor: NSObject {

    @IBOutlet weak var cityCode: UITextField!
    @IBOutlet weak var cityCodeLabel: UILabel!

    func setup() {
        Weather().cityCode.bidirectionalBindTo(cityCode.bnd_text)
        Weather().cityCode.bindTo(cityCodeLabel.bnd_text)
    }
}

UITableを利用する時でも同様にできます。
コードは以下の通りです。
Controllerでは、UITableViewDelegateを適応します。

SecondController.swift
class SecondController: UIViewController, UITableViewDelegate {
    @IBOutlet var secondPresentor: SecondPresentor!

    override func viewDidLoad() {
        super.viewDidLoad()
        self.secondPresentor.tableView.delegate = self
        secondPresentor.setup()
        Weather().errorState.observeNew { state in
            switch state {
            case .NotFound:
                let alertController = UIAlertController(title: "検索結果がありません", message: "", preferredStyle: .Alert)
                let okAction = UIAlertAction(title: "OK", style: .Cancel) { alert in
                    print(alert)
                }
                alertController.addAction(okAction)
                self.presentViewController(alertController, animated: true, completion: nil)
                break
            default:
                break
            }
        }
    }

    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        if let indexPath = self.secondPresentor.tableView.indexPathForSelectedRow {
            self.secondPresentor.tableView.deselectRowAtIndexPath(indexPath, animated: true)
        }
    }


    // MARK: - UITableViewDelegate

    func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        let name = Weather().dataSource[indexPath.section][indexPath.row]
        print(name.observableType().telop.value + " selected!")
    }
}

PresentorではUITableViewCellの作成とBondとの繋ぎこみをするぐらいです。
また具体的な処理はUI層の下のレイヤー(アプリケーション層やドメイン層など)に委譲するだけにします。

SecondPresentor.swift
class SecondPresentor: NSObject {
    @IBOutlet weak var tableView: UITableView!

    func setup() {
        let service = WeatherService()
        service.fetchWeather()
        Weather().dataSource.bindTo(self.tableView) { (indexPath, dataSource, tableView) -> UITableViewCell in
            let cell = tableView.dequeueReusableCellWithIdentifier("MyUITableViewCell", forIndexPath: indexPath)
            let forecast = dataSource[indexPath.section][indexPath.row]
            forecast.observableType().telop.bindTo(cell.textLabel!.bnd_text).disposeIn(cell.bnd_bag)
            return cell
        }
    }
}

class MyUITableViewCell: UITableViewCell {
    override func prepareForReuse() {
        super.prepareForReuse()
        bnd_bag.dispose()
    }
}

まとめ

この方法はBondとの相性がいいので使いましたが、Bondを使わなくても同じようにControllerの役割を分割できます。

そして、Presentorに関して言えば、ViewControlerに依存しないので単体テストもしやすくなります。

簡単な画面だと面倒なだけですが、複雑な画面でFat Controllerを解決したい場合の一つの方法として覚えておいても損はないでしょう。

22
22
0

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
22
22