背景
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を実装すれば、追加のコード無しでインスタンスを簡単に生成できるようになりました。便利ですね。
参考