昨日今日で自分用のSwiftライブラリの整理をしている中で、 StoryboardからViewControllerを生成するProtocol をリファクタリングしつつ綺麗にまとめたので、記事にしてみようと思います。
この、 StoryboardからViewControllerを生成するProtocol 自体は前から色々な人がQiita等にまとめていて、物によっては structやenum を活用した方法もあったりします。
そうした記事を参考に自分も自分が扱いやすいようにProtocolを作って運用していましたが、改めてリファクタリングしてより扱いやすくまとめたので、紹介してみようと思います。
ソース
public protocol StoryboardInstantiatable {
static var storyboardName: String { get }
static var viewControllerIdentifier: String? { get }
static var bundle: NSBundle? { get }
}
extension StoryboardInstantiatable where Self: UIViewController {
public static var storyboardName: String {
return String(self)
}
public static var viewControllerIdentifier: String? {
return nil
}
public static var bundle: NSBundle? {
return nil
}
public static func instantiate() -> Self {
let loadViewController = { () -> UIViewController? in
let storyboard = UIStoryboard(name: storyboardName, bundle: bundle)
if let viewControllerIdentifier = viewControllerIdentifier {
return storyboard.instantiateViewControllerWithIdentifier(viewControllerIdentifier)
} else {
return storyboard.instantiateInitialViewController()
}
}
guard let viewController = loadViewController() as? Self else {
fatalError([
"Failed to load viewcontroller from storyboard.",
"storyboard: \(storyboardName), viewControllerID: \(viewControllerIdentifier), bundle: \(bundle)"
].joinWithSeparator(" ")
)
}
return viewController
}
}
このProtocolの利点
以下の点に焦点を当てて作っています。
- storyboardName 、 viewControllerIdentifier 、 bundle を宣言し、それぞれに規定の実装を与えておき、必要があればオーバーロードしてもらう形に
- SelfがUIViewControllerとそのサブクラスであるという制約を加える事で、関係ないクラスへの適応を防止
- StoryboardからViewControllerが作れない時は
fatalError
でクラッシュさせるように - Storyboardの名前とViewControllerの名前が一緒且つ、ViewControllerが
initial viewcontroller
指定されている場合、Protocolを適応するだけで良い - Storyboardの名前とViewControllerの名前が異なる場合は、
storyboardName
を指定するだけで良い - StoryboardIDが指定されているViewControllerをStoryboardから生成する事も可能
基本的には、よく言われる 1VC = 1Storyboard
として運用するのを前提にしているので、 Storyboardの名前とViewControllerの名前を一緒 にして運用する時に一番扱いやすいようにというのを考えています。
ですが、場合によってはファイル名が異なっていたり、initial viewcontroller
指定がされておらず、StoryboardID
が指定されている場合もあるので、その場合にも柔軟に対応できるように作っています。
1VC = 1Storyboard
での運用を楽にするために、 storyboardName には規定の実装として、自身の型の名前を返すようにしています。
その他の viewControllerIdentifier 、 bundle は、規定の実装としてnil
を与えています。
呼び出す時は、何れの場合でも、 instantiate()
を呼び出せば良いようにしています。
使い方1 : 1VC = 1Storyboardで名前が一致している場合
HogeViewController.storyboard
とHogeViewController
クラスの用に、ファイル名とクラス名が一致している場合は、
HogeViewController
に StoryboardInstantiatable を適合してあげるだけです。
class HogeViewController: UIViewController, StoryboardInstantiatable {
var value: String
// ...
}
使う時は、
let vc = HogeViewController.instantiate()
vc.value = "sample"
としてあげるだけです。
更に、instantiate()
で生成した時点で、vc
がHogeViewController
の型であることが推論されるので、キャストしたりする必要はありません。
使い方2 : Storyboardの名前とViewControllerの名前が違う場合
HogeHoge.storyboard
とHogeViewController
のように名前が違う場合は、以下のようにしてあげます。
class HogeViewController: UIViewController, StoryboardInstantiatable {
// storyboardファイル名を定義
static var storyboardName: String {
return "HogeHoge"
}
var value: String
// ...
}
使う時は、先ほどと同様、
let vc = HogeViewController.instantiate()
vc.name = "sample"
で生成が可能です。
使い方3 : Storyboardに複数ViewControllerがあって、その中の特定のViewControllerを生成する
使い方2と同様、HogeHoge.storyboard
とHogeViewController
のように名前が違い、更にStoryboard内に複数ViewControllerが配置されていて、
HogeViewController
に、HogeIdentifier
というStoryboardIDを指定している場合は、以下のようにします。
class HogeViewController: UIViewController, StoryboardInstantiatable {
// storyboardファイル名を定義
static var storyboardName: String {
return "HogeHoge"
}
public static var viewControllerIdentifier: String? {
return "HogeIdentifier"
}
var value: String
// ...
}
これで、複数Storyboard内にViewControllerがあっても問題なく生成できます。
ちなみに
R.Swiftというライブラリもあり、上記と似たようなことをできますし、StoryboardID等が変わったとしても自動的に補完が働いてうまい具合にやってくれますが、
- generateに時間がかかる
- 複数人開発をするとコンフリクトしやすいかも?
- 多機能すぎるので、特定の機能だけ使いたい時に、ちょっとライブラリの規模が大きいかもしれない
といったことがあるので、僕は使っていません。(決してネガキャンではないです..)