ワン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
にチェックを入れた状態にするとUIStoryboard
のinstantiateInitialViewController()
が使えるようになる。更に、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.storyboard
とViewControllerTwo.storyboard
を作る。
それぞれのCustom Classに同じ名前のクラスを指定する。
Et voilà!
// 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.storyboardのViewController
import UIKit
class ViewControllerOne : UIViewController {
}
// ViewControllerTwo.storyboardのViewController
import UIKit
class ViewControllerTwo : UIViewController {
}