文字っぽいの。

文字を書いています。写真も混ざります。

Storyboardを利用したViewControllerのインスタンス生成を楽にする

背景

1つのStoryboardに1つのViewControllerという運用をしていると、ViewControllerのインスタンス生成するコードを書くことが多くなると思います。雑に書くと、

let storyboard = UIStoryboard(name: "SomeViewController", bundle: .main)
let vc = storyboard.instantiateInitialViewController() as! SomeViewController
present(vc, animated: true, completion: nil)

こんな感じでViewControllerを作ってモーダル表示できますね。

さて、これだと生成する度にこのコードを書かないといけないので大変です。なので生成したいViewController側に書いちゃいましょう。

final class SomeViewController: UIViewController {
    
    class func viewController() -> SomeViewController {
        let storyboard = UIStoryboard(name: "SomeViewController", bundle: .main)
        let viewController = storyboard.instantiateInitialViewController() as! SomeViewController

        return viewController
    }

}

...

// 使う時
let vc = SomeViewController.viewController()
present(vc, animated: true, completion: nil)

1回定義してしまえば使う時はさっと使えて便利です。実装も SomeViewController 側にまとめられるので安心。

ただ、この方法だとViewControllerを作る度に class func を作る必要があってわりと手間です。また、よくある事故として UIStoryboard(name: "SomeViewController", bundle: .main) のnameを変え忘れ問題があります。めんどくさいので書かなくても済むようにしたいです。

解決法

まずは次のような protocol-extension を書きます。

protocol Instantiatable {
    static var storyboardName: String { get }
}

extension Instantiatable where Self: UIViewController {
    static var storyboardName: String {
        return ""
    }

    private static var _storyboardName: String {
        if storyboardName.isEmpty {
            return className
        } else {
            return storyboardName
        }
    }

    private static var storyboard: UIStoryboard {
        return UIStoryboard.init(name: _storyboardName, bundle: nil)
    }

    private static var className: String {
        return String(describing: Self.self)
    }

    static func instantiateFromStoryboard() -> Self {
        guard let vc = storyboard.instantiateInitialViewController() as? Self else {
            fatalError("Can no instantiate \(Self.className) from \(storyboardName).storyboard")
        }
        return vc
    }

    static func instantiateFromStoryboard(withIdentifier id: String) -> Self {
        guard let vc = storyboard.instantiateViewController(withIdentifier: id) as? Self else {
            fatalError("Can no instantiate \(Self.className) from \(storyboardName).storyboard with id: \(id)")
        }
        return vc
    }
}

あとは自作のViewControllerで、

final class SomeViewController: UIViewController, Instantiatable {
// ...
}

とすれば

let vc = SomeViewController.instantiateFromStoryboard()
present(vc, animated: true, completion: nil)

と使うことができます。簡単ですね。もし、Storyboard IDを元にしたいのであれば

let vc = SomeViewController.instantiateFromStoryboard(withIdentifier: "SomeID")

とすればよいです。また、.storyboardの名前がViewControllerのクラス名と異なっている(今回の例の場合は SomeViewController.storyboard ではない)場合には

final class SomeViewController: UIViewController, Instantiatable {
    static let storyboardName = "OtherStoryboardName"

    //...
}

のように storyboardName を指定すれば良いです。この場合は SomeViewController.storyboardではなく OtherStoryboardName.storyboard を見に行きます。

まとめ

これで Instantiatable protocolを実装すれば、追加のコード無しでインスタンスを簡単に生成できるようになりました。便利ですね。

参考