Storyboard
Protocol
Swift
SwiftDay 4

Protocol oriented Storyboard

はじめに

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
  }
}

やりました。StoryboardInstantiatableViewControllerに実装するだけで、instantiate()が使えるようになったと思います。
ViewControllerfinal 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なコードが書きやすくなっているはず!