背景
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を実装すれば、追加のコード無しでインスタンスを簡単に生成できるようになりました。便利ですね。
参考
- Protocolを利用してStoryboardやXibファイルからインスタンスを生成する - Qiita
- もう1つ英語の記事を読んだ記憶があるんですが見つけられず……