StoryboardからViewControllerを生成するProtocolを今更ながら出来る限り綺麗にまとめてみた

Thursday, May 5, 2016

昨日今日で自分用の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の利点

以下の点に焦点を当てて作っています。

  • storyboardNameviewControllerIdentifierbundle を宣言し、それぞれに規定の実装を与えておき、必要があればオーバーロードしてもらう形に
  • 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 には規定の実装として、自身の型の名前を返すようにしています。
その他の viewControllerIdentifierbundle は、規定の実装としてnilを与えています。
呼び出す時は、何れの場合でも、 instantiate() を呼び出せば良いようにしています。

使い方1 : 1VC = 1Storyboardで名前が一致している場合

HogeViewController.storyboardHogeViewControllerクラスの用に、ファイル名とクラス名が一致している場合は、
HogeViewControllerStoryboardInstantiatable を適合してあげるだけです。

class HogeViewController: UIViewController, StoryboardInstantiatable {
    var value: String
    // ...
}

使う時は、

let vc = HogeViewController.instantiate()
vc.value = "sample"

としてあげるだけです。
更に、instantiate()で生成した時点で、vcHogeViewControllerの型であることが推論されるので、キャストしたりする必要はありません。

使い方2 : Storyboardの名前とViewControllerの名前が違う場合

HogeHoge.storyboardHogeViewControllerのように名前が違う場合は、以下のようにしてあげます。

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.storyboardHogeViewControllerのように名前が違い、更に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に時間がかかる
  • 複数人開発をするとコンフリクトしやすいかも?
  • 多機能すぎるので、特定の機能だけ使いたい時に、ちょっとライブラリの規模が大きいかもしれない

といったことがあるので、僕は使っていません。(決してネガキャンではないです..)

techSwiftTipsprotocol

会社のTech Blogを書きました

OptionalなBoolの判定をCustom Operatorで解決する