#はじめに
UIKit、特にStoryboardとかその辺全然Swiftyじゃないよね💩という話です。
俺はSwiftgen派だしな…って人も、protocolを使う上でのヒントが見つかるかもしれないので見ていただければ。
全部把握してるし別に…って人は回れ右です。貴方は1行でも多くコードを書くべきだ。
#結果
こんな感じのコードをprotocolを定義するだけで書けるようになります。
いずれもダウンキャストは必要ありません。
// Storyboard or Nib
let vc = ViewController.instantiate()
let view = View.instantiate()
// deque
let cell = tableView.dequeReusableCell(type: Cell.self, for: indexPath)
// segue
self.perform(segue: destination(ViewController.self), prepare: { (vc) in
vc...
})
絶賛WIPなコードはこちら
https://github.com/tarunon/Instantiate
#ダウンキャストと曖昧さ
UIKit、特にStoryboardやNibの周りでは、しばしばダウンキャストが必要になるコードが出てきます。例えば、
let viewController = UIStoryboard(name: "ViewController", bundle: nil).instantiateInitialViewController() as! ViewController
let view = UINib(nibName: "View", bundle: nil).instantiate(withOwner: nil, options: nil)[0] as! View
のような。さて、このコードの何が邪悪かというと、曖昧さが高くて本来Swiftが持っているはずの言語の性能を殺している、コンパイラの性能を活かしきれていないところにあります。
// ↓🤔 ↓🤔
let viewController = UIStoryboard(name: "ViewController", bundle: nil).instantiateInitialViewController() as! ViewController
わかりやすいように'🤔'を付けました。
例えばこのViewController
クラスをStoryboardから生成する場合、この"ViewController"
というStoryboardNameは、実行時に変更され得るパラメータでしょうか?
恐らく、適切にViewControllerクラスを設計している貴兄らのプロジェクトにおいて答えは否、固定でしょう。
一つのクラスを複数のStoryboardから使うなんてヤケクソは、やるべきではありません。
そして、このようなコードを使ってる人もいるのでは。
extension ViewController {
static let storyboardName: String { return "ViewController" }
static func instantiate() -> ViewController {
return UIStoryboard(name: storyboardName, bundle: nil).instantiateInitialViewController() as! ViewController
}
}
ViewControllerを使う分には安心してViewController.instantiate()
を呼び出せる世界になりました。
しかし、このextension
を毎回クラスに実装するのはイマイチです。
我々にはprotocol
というブキがあります。
#Protocol Oriented...?
さて、先程のコードを抽象化してprotocol
として切り出してみましょう。
protocol StoryboardInstantiatable {
static var storyboard: UIStoryboard { get }
static func instantiate() -> Self
}
宣言はこれで十分でしょう。
そして実装はextension
に書き出すことができます。
extension StoryboardInstantiatable where Self: UIViewController {
static var storyboard: UIStoryboard {
return UIStoryboard(name: NSStringFromClass(self).components(separatedBy: ".").last!, bundle: Bundle(for: self))
}
static func instantiate() -> Self {
return storyboard.instantiateInitialViewController() as! Self
}
}
やりました。StoryboardInstantiatable
をViewController
に実装するだけで、instantiate()
が使えるようになったと思います。
ViewController
をfinal class
にしないと怒られますが、まあ些細な問題でしょう。(まさかStoryboardでインスタンス化するViewContorllerをサブクラス化する人なんていませんよね!?)
しかし、もう少し、適切な抽象化について考えると、より良くなるかもしれません。
#Protocol Oriented
instantiate()
というfunctionですが、これは自身以外のオブジェクトからインスタンスを生成する役割を持つものと定義できそうです。
例えば、Storyboardに限らず、Nibからインスタンスを生成する場合もありえます。
そうすると、StoryboardInstantiatable
に直接instantiate()
を生やすのではなく、
protocol Instantiatable {
static func instantiate() -> Self
}
protocol StoryboardInstantiatable: Instantiatable {
static var storyboard: UIStoryboard { get }
}
このように型を分割したほうが取り回しが効きそうです。
フィールドとしてのStoryboardを使いたいだけなのに、instantiate()
が付いて来る、というのもイマイチです。(ホントはそれ以外の用途は無さそうですが)
Storyboardはともかく、Nibであれば、TableViewのReuseに使うという用途があり得ます。
そうすると、storyboardも分離しておいたほうが便利かもしれません。
protocol Instantiatable {
static func instantiate() -> Self
}
protocol StoryboardType {
static var storyboard: UIStoryboard { get }
}
protocol StoryboardInstantiatable: Instantiatable, StoryboardType {
}
良くなってきました。このように、UIKitが提供している不適切な抽象化を、一旦固めてしまってから、再度適切な抽象化をすると、概ねSwiftで気持ちよくコードが書ける環境になっていくと思います。
Segueについては前回書いたので、Reusable周りを書きたかったんですが、どうやらMP切れのようです。また今度。
#Conclusion
SwiftのProtocolの機能はとても強力で素晴らしい機能です。
しかし、現状のUIKitは素晴らしくありません。多くの不適切な抽象化によってコードが曖昧になり、Swift本来のコンパイラの能力を殺しています。
もしあなたが新規プロジェクトに携わっているなら、不適切な抽象化を一度無くしてしまって、再度自身で適切な抽象化(protocol作り)をしてみると良いと思います。
きっと、Swiftyなコードが書きやすくなっているはず!