最近剛到新公司,這裡的專案大量的使用Storyboard,在前公司時,因為多人協作的關係,並沒有使用Storyboard 在進行開發,因此,結合了一些已閱讀過的觀念,順便來實作看看。

原本的問題

原本使用Storyboard 要拿到一個viewController 的instance 時,需要像這樣:

func foo() {
    let storyboard = UIStoryboard(name: "MainStoryboard", bundle: nil)
    let initViewController = storyboard.instantiateInitialViewController()
    let viewController = storyboard.instantiateViewController(withIdentifier: "AnIdentifierForViewController") as! ViewController
    // Do something...
}

這邊有幾個缺點:

  • Storyboard 與ViewController 的Identifier 都是字串,重複使用時容易typo,對開發來說造成許多變因,以經驗上來說挺危險的
  • 使用了as! 來做downcast,但不使用強制轉型又顯得哆嗦

改善

透過Swift 的Protocol extension,我們可以大幅改善這些缺點:

enum StoryboardCases: String {

    case main = "MainStoryboard"
    case other = "OtherStoryboard"

}


protocol Storyboardable: class {
    static var defaultStoryboardName: String { get }
}

extension Storyboardable where Self: UIViewController {

    static var defaultStoryboardName: String {
        return String(describing: self)
    }

    static func viewController(from storyboard: StoryboardCases) -> Self {
        let storyboard = UIStoryboard(name: storyboard.rawValue, bundle: nil)

        guard let viewController = storyboard.instantiateViewController(withIdentifier: defaultStoryboardName) as? Self else {
            fatalError("Could not instantiate initial storyboard with name: \(defaultStoryboardName)")
        }

        return viewController
    }

    static func initialViewController(from storyboard: StoryboardCases) -> Self {
        let storyboard = UIStoryboard(name: storyboard.rawValue, bundle: nil)

        guard let viewController = storyboard.instantiateInitialViewController() as? Self else {
            fatalError("Could not instantiate initial view controller")
        }

        return viewController
    }

}

extension UIViewController: Storyboardable {}

用起來像這樣:

func foo() {
    // 使用`MyViewController` 當作identifier,並在`main` storyboard 裡面尋找
    let controller = MyViewController.viewController(from: .main)

    // or
    // 在`other` storyboard 當中,尋找initialViewController 
    let otherController = OtherViewController.initialViewController(from: .other)
}

如此一來,便可藉由Swift 的強型別特性,讓compiler 知道我們的型別,也就不需要再使用as! 進行downcast,也可以避免使用字串(必須在enum 當中統一管理、複用)。

這用法的前提是,每當我們新增了新的Storyboard 時,都必須要到StoryboardCases 當中去新增,並且Storyboard ID 要與ViewController 的名字相同。

使用上來說,若團隊一開始就進行約束,後者可能不需要做任何修改,但必須要「手動」將Storyboard 加入StoryboardCases 這件事的確是頗有不便。🤔

其他解法

R.swift 就有如為此而生,寫過Android 的開發者大概都會對R.* 有點印象🤣,這個third party 用途也非常類似,他透過build script 運行腳本,來產生所有的資源檔案(也就是Meta programming 囉),其中就包含了Storyboard,其他的檔案型別可以直接參考範例

不過,我負責的專案是一個mix and match 的專案,當中包含許多的objc 檔案,R.swift 並不支援這樣的專案,因此我也只能含淚棄用,期待有一天可以將所有的code 轉成Swift 拉。

參考

Simpler iOS Storyboard Instantiation – codeburst

blog