最近剛到新公司,這裡的專案大量的使用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