StoryboardからUIViewControllerのインスタンス化をDRYに


ワンStoryboardワンViewController制

複数のViewControllerを同じStoryboard内に定義すると不都合がいろいろ出てくる。その為、だいたいはViewController毎にStoryboardを分けるようにしている。

ViewControllerをインスタンス化するのに、だいたい次のようなコードを使うでしょう。

let sb = UIStoryboard(name: "SomeStoryboard", bundle: nil)
let vc = sb.instantiateViewController(withIdentifier: "SomeViewController")
self.present(vc, animated: true, completion: nil)

いちいち"SomeStoryboard""SomeViewController"を取り回すのがダルい、況してや名前を変えたりリファクタリングしたりする時を考えると本当に気が滅入る。

これをなんとかDRYにしたい。

DRY化 第一弾

Is Initial View Controllerにチェックを入れた状態にするとUIStoryboardinstantiateInitialViewController()が使えるようになる。更に、Storyboard名とViewController名とを同じにすることで管理し易くなる(整理整頓)。

先ずはStoryboardとViewControllerの名前を一致させ、Is Initial View Controllerにチェックを入れておこう。

そんな状態であれば、さっきのコードが次のようになろう。

let sb = UIStoryboard(name: "SomeViewController", bundle: nil)
let vc = sb.instantiateViewController()
self.present(vc, animated: true, completion: nil)

文字列がまだ残ってしまっているが、少しスマートになったね。
とはいえやはり、文字列を直接扱いたくないよね。

DRY化 第二弾

列挙型にするといった手法を選ぶ事もできる。その場合は次のようになろう。

enum Screen : String {
    case example = "SomeViewController"
}
let sb = UIStoryboard(name: Screen.example.rawValue, bundle: nil)
let vc = sb.instantiateViewController()
self.present(vc, animated: true, completion: nil)

これでは意味のある名前が付いた定数のようなものができた。
しかし、まだ文字列があるのは気に食わないし、謎のrawValueもぶっちゃけ目障り。

それにすべての画面名が静的に羅列されているのもイヤだな。
メンテナンスしたくない。

型名を動的に取得する

Swiftには型名を文字列で取得する方法があるんじゃない?

$ swift
Welcome to Apple Swift version 4.0.3 (swiftlang-900.0.74.1 clang-900.0.39.2). Type :help for assistance.
  1> class SomeViewController { }
  2> String(describing: SomeViewController.self)
$R0: String = "SomeViewController"
  3> ^D

おっ!できた!

DRY化 第三弾

let sb = UIStoryboard(name: String(describing: SomeViewController.self), bundle: nil)
let vc = sb.instantiateViewController()
self.present(vc, animated: true, completion: nil)

よっしゃ!完全に文字列の直接的な扱いを撲滅できたね。

ただ、今度はいちいちString(describing: Foobar.self)を書かなくちゃならない。

汎用的な書き方でき・・・・・・・る!
Genericsが使えるじゃん。

Super DRY

func getVcInstance<T: UIViewController>() -> T? {
    // ViewControllerの型名を取得する
    let name = String(describing: T.self)
    // ViewControllerの型の格納したBundleを取得する
    let bundle = Bundle(for: T.self)
    // Storyboardを取得し、TとしてViewControllerをインスタンス化してみる
    let storyboard = UIStoryboard(name: name, bundle: bundle)
    return storyboard.instantiateInitialViewController() as? T
}

guard let vc: SomeViewController = getVcInstance() else { return }
self.present(vc, animated: true, completion: nil)

もう、列挙する必要全く無し!
ありがたや~ありがたや~

これでコードが非常にスッキリする。

満足!

Single View Appの例

新たにViewControllerOne.storyboardViewControllerTwo.storyboardを作る。
それぞれのCustom Classに同じ名前のクラスを指定する。

Et voilà!

MainViewController.swift
// Main.storyboardのViewController

import UIKit

class ViewController: UIViewController {
    @IBAction func vcOneButtonTapped(_ sender: Any) {
        guard let vc: ViewControllerOne = getVcInstance() else { return }
        self.present(vc, animated: true, completion: nil)
    }

    @IBAction func vcTwoButtonTapped(_ sender: Any) {
        guard let vc: ViewControllerTwo = getVcInstance() else { return }
        self.present(vc, animated: true, completion: nil)
    }

    func getVcInstance<T: UIViewController>() -> T? {
        // ViewControllerの型名を取得する
        let name = String(describing: T.self)
        // ViewControllerの型の格納したBundleを取得する
        let bundle = Bundle(for: T.self)
        // Storyboardを取得し、TとしてViewControllerをインスタンス化してみる
        let storyboard = UIStoryboard(name: name, bundle: bundle)
        return storyboard.instantiateInitialViewController() as? T
    }
}
ViewControllerOne.swift
// ViewControllerOne.storyboardのViewController

import UIKit

class ViewControllerOne : UIViewController {
}
ViewControllerTwo.swift
// ViewControllerTwo.storyboardのViewController

import UIKit

class ViewControllerTwo : UIViewController {
}