なりゆき
segueのidentifierってどうやってつけてるんだろみんな 文字リテラル使いたくないから行き先のViewControllerのクラス名使ってString(HogeViewController.self)ってやってるけど
— ひらり (@hiragram) 2016年4月4日
とりあえず事の顛末はツイートを辿ってもらえば全部理解できるんですが折角なので共有をします。
クラス名使ってると、UINavigationControllerとかUITabBarControllerに遷移しようものなら即死ですし、でなくとも複数の画面から指してる場合も死ねますね。よろしくない。
せっかくSwift使ってるんですから幸せになりたいものです。
レベル1 (enum + extension)
Protocolを宣言してUIViewControllerにextensionを生やします。
protocol SegueType {
var rawValue: String { get }
}
extension UIViewController {
func performSegue(segue: SegueType, sender: AnyObject?) {
performSegueWithIdentifier(segue.rawValue, sender: sender)
}
}
ViewControllerにSegueを実装してStringとSegueTypeを宣言、caseにstoryboardで定義したsegue identifierを列挙します。
class MyViewController: UIViewController {
enum Segue: String, SegueType {
case ToNext
}
}
myvc.performSegue(MyViewController.Segue.ToNext, sender: nil)
ね、簡単でしょ?
enumにstoryboardに書いたidentifierを移すだけの簡単なお仕事です。
え?prepareForSegueが幸せじゃない?
しょうがないにゃぁ(´・ω・`)
レベル2 (associatedtype + extension)
今度はassociatedtypeをゴリゴリ使ってProtocolとextensionを作っていきます
protocol SegueType {
associatedtype Target: UIViewController
static var identifier: String { get }
}
protocol TypedSegueSourceType: class {
var handlers: [String: (UIViewController, sender: AnyObject?) ->()] { get set }
func addSegueHandler<S: SegueType>(segue: S.Type, handler: (S.Target, sender: AnyObject?) -> ())
}
extension TypedSegueSourceType where Self: UIViewController {
func addSegueHandler<S: SegueType>(segue: S.Type, handler: (S.Target, sender: AnyObject?) -> ()) {
handlers[segue.identifier] = { controller, sender in
handler(controller as! S.Target, sender: sender)
}
}
func _prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let handler = handlers[segue.identifier!] {
handler(segue.destinationViewController, sender: sender)
}
}
}
SegueType
にTarget
が加わりました。これを使ってprepareForSegue
で型推論を効かせます。
TypedSegueSourceType
という新しいProtocol
を定義しました。Handlerを追加するfunc、格納するhandlers
を宣言します。
handlers
を書き込み可能にするためにclass only protocolになっています。(どっちにしてもUIViewController
に付加するので問題ありません)
そして、 extension TypedSegueSourceType where Self: UIViewController
に実装を書きます。
最後はHandlerをコールする_prepareForSegue
を書いて準備は終了。
class MyViewController: UIViewController, TypedSegueSourceType {
struct Segue {
struct ToNext: SegueType {
typealias Target = NextViewController
static var identifier = "ToNext"
}
}
var handlers: [String : (UIViewController, sender: AnyObject?) -> ()] = [:]
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
_prepareForSegue(segue, sender: sender)
}
}
myvc.addSegueHandler(MyViewController.Segue.ToNext.self) { (target, sender) in
// 型推論が効いてtarget: NextViewControllerになっています。ハッピー★
}
簡単ですね、Segueの呼び出し元となるVCにTypedSegueSourceType
を実装して、handlers
とprepareForSegue
をちょいと書けば全部ヨロシク動いちゃいます。すごい。
protocolとextensionの実装にしているのは、対象がUIViewControllerをストレートに継承出来ない場合があるからです。(Navigation、TabBar、etc)
何、UINavigationCotnroller
とUITabBarController
はどうするか?
レベル3 (Generics NavigationController)
UINavigationController以下に同じ型のViewControllerが格納される場合に限定しますが、GenericsなUINavigationControllerを作り、それを継承して束縛することでstoryboard上で使うことが ~~出来ます。~~出来ません。ポンコツ乙
正しくはprotocolとassociatedtypeを使って束縛します。ゴメンナサイ
protocol TypedNavigationController: class {
associatedtype T: UIViewController
var genericsViewControllers: [T] { get set }
var genericsTopViewController: T? { get }
var genericsVisibleViewController: T? { get }
}
extension TypedNavigationController where Self: UINavigationController {
var genericsViewControllers: [T] {
get {
return self.viewControllers.map { $0 as! T }
}
set {
self.viewControllers = newValue
}
}
var genericsTopViewController: T? {
return self.topViewController as? T
}
var genericsVisibleViewController: T? {
return self.visibleViewController as? T
}
}
class NextViewController: UINavigationController, TypedNavigationController {
typealias T = ChildViewController
}
この場合、NextViewController
はそのままstoryboardで使えます。
それ以下の型をチェックしてくれるわけではありませんが、GenericsなViewControllerをstoryboardで使うという欲求は満たせるでしょう。
もちろん、世の中にはもはや同じViewControllerしか格納されないNavigationは都市伝説なわけですし、TabBarControllerには対応できない。
TabBarControllerはせいぜい5つが限界なので、2-5までのTypedTabBarControllerのprotocolとextensionを作ればいいです。
NavigationControllerは、ChildViewControllerにおいてEmbedFrameとenumを上手く組み合わせれば、switchで分岐して推論も出来ます。
最も、NavigationController触る上で最も大事なのはrootViewControllerの型が保証されることなので、それさえできれば良い気もしますけどね。
※サンプルコード内にて!
を多用しています。面倒くさいから使いましたが普通はLint等で弾くことをおすすめします。
まとめ
Storyboardから触る諸々も、予め頑張っておけば型推論が効いて幸せになれます。
RxSwiftとか使うとこの辺がもっと幸せに書けるようになるんですよね(宣伝)
やってることはStoryboardと型推論を人の手で繋げているだけなので、型安全とか痴がましいことは口が裂けても言えないです。でも出来るだけ幸せになりたいんじゃ~