作者:Andyy Hope,原文鏈接,原文日期:2016-01-24
譯者:jseanj;校對(duì):saitjr;定稿:CMB
幾周前,我無(wú)意發(fā)現(xiàn) Guille Gonzalez 寫的一篇文章,介紹了如何用協(xié)議和擴(kuò)展讓 UITableViewCell 的注冊(cè)和重用更安全。
看完這篇文章后我非常驚嘆,因?yàn)椴恍枰蕾嚴(yán)^承,只需要協(xié)議擴(kuò)展和泛型就可以非常容易的實(shí)現(xiàn)自定義的行為。自從 WWDC15,我們已經(jīng)聽到關(guān)于 Swift 如何是一門面向協(xié)議的語(yǔ)言,而我只是一知半解,如果你懂我的意思的話。而就在此時(shí)我終于明白他們?cè)谥v的是什么了。
在我花費(fèi)大量時(shí)間做的應(yīng)用中有一個(gè)大的 storyboard,使用起來(lái)令人難以置信的繁瑣,所以我最后決定將它分離開。將一個(gè)巨大的 UIStoryboard 分成眾多小的 UIStoryboard,然后我只需要在我的代碼中用不同的字符串去實(shí)例化 UIStoryboard,但是這樣從來(lái)不安全。
字符串
你留在家中的無(wú)聲殺手
let name = "News"
let storyboard = UIStoryboard(name: name, bundle: nil)
let identifier = "ArticleViewController"
let viewController = storyboard.instantiateViewControllerWithIdentifier(identifier) as! ArticleViewController
上面的代碼中,我們創(chuàng)建了一個(gè)名稱為 “News” 的 UIStoryboard 實(shí)例,這會(huì)在工程資源目錄下找名稱為 “News.storyboard” 的文件。但是,如果有一個(gè)更復(fù)雜的名稱比如 “Onomatopoeia”,由于這個(gè)詞非常奇怪并且不尋常,導(dǎo)致我為了文章的書寫必須查查怎么拼寫這個(gè)詞。可以想象,在工程代碼中如果我要不停的去猜如何拼寫類似的詞,這將是非常愚蠢的,但實(shí)際上人們就是在做著瘋狂的事情。
不知道怎么拼寫就算了,更糟的是由于它是一個(gè)字符串,Xcode 的語(yǔ)法檢查器并不會(huì)檢測(cè)出來(lái)拼寫錯(cuò)誤,因此你只能在運(yùn)行時(shí)才可能發(fā)現(xiàn)這種錯(cuò)誤。唉!
如何才能讓 UIStoryboard 更安全
全局字符串常量
不,永遠(yuǎn)不。剛開始這聽起來(lái)是一個(gè)好想法,因?yàn)槟阒恍枰x一次常量就可以在任何地方使用。如果你想改變常量的值,只需要更改一處就可以讓工程中所有使用該常量的地方發(fā)生變化。
但是這樣你會(huì)少了一個(gè)變量名,你會(huì)對(duì)經(jīng)常要重用一個(gè)變量名而感到驚訝。你曾經(jīng)嘗試過(guò)給一個(gè) NSObject 的子類的屬性命名為 “description” 嗎?這時(shí)你就知道我的意思了。如果 storyboards 使用多個(gè)字符串常量標(biāo)識(shí)符的話,就會(huì)喪失一致性,如果它們定義在工程中的不同地方,也會(huì)使查找和合并它們變得更困難。
定義一個(gè)全局字符串常量還有諸多壞處,但是為了繼續(xù)討論文章中要提到的精華,所以我們將會(huì)忽略這些壞處。
關(guān)聯(lián)的 Storyboard 名稱
首要原則是你的 storyboard 應(yīng)該以它包含的模塊命名。例如,如果一個(gè) storyboard 包含的控制器是關(guān)于新聞的,那么就把 storyboard 文件命名為 “News.storyboard”。
統(tǒng)一 Storyboard 標(biāo)識(shí)符
當(dāng)你打算在你的控制器上使用 Storyboard 標(biāo)識(shí)符時(shí),通常的做法是使用類名作為標(biāo)識(shí)符。比如 “ArticleViewController” 作為 ArticleViewController 的標(biāo)識(shí)符。這將會(huì)減少你和同事們的負(fù)擔(dān),你和你的同事們不必再去想和記憶統(tǒng)一標(biāo)識(shí)符或者命名規(guī)范。
枚舉
可以考慮將枚舉作為統(tǒng)一的、中心全局化的 UIStoryboard 字符串標(biāo)識(shí)符。為了讓 storyboard 實(shí)例化對(duì)象變得真正的安全,我們可以創(chuàng)建一個(gè) UIStoryboard 類的擴(kuò)展,其中定義了工程中不同的 storyboard 文件。
extension UIStoryboard {
enum Storyboard : String {
case Main
case News
case Gallery
}
}
正如你所見,所有的內(nèi)容在工程中都是統(tǒng)一并且中心化的。實(shí)例化也更加安全,當(dāng)你敲擊標(biāo)識(shí)符時(shí) Xcode 也會(huì)自動(dòng)補(bǔ)全。
let storyboard = UIStoryboard(name: UIStoryboard.Storyboard.News.rawValue, bundle: nil)
這段代碼可以順暢的編譯并運(yùn)行,但是語(yǔ)法卻很丑陋。因此我們?cè)偕钊氲睾?jiǎn)化一下語(yǔ)法,在 UIStoryboard 擴(kuò)展中創(chuàng)建一個(gè)便利構(gòu)造方法:
convenience init(storyboard: Storyboard, bundle: NSBundle? = nil) {
self.init(name: storyboard.rawValue, bundle: bundle)
}
...
let storyboard = UIStoryboard(storyboard: .News)
你將會(huì)注意到,bundle: 參數(shù)默認(rèn)是 nil,因此在調(diào)用構(gòu)造方法時(shí)可以忽略 bundle: 參數(shù)。
這樣做的原因是如果你傳 nil 給 bundle 參數(shù),UIStroyboard 類會(huì)去 main bundle 中查找資源,所以給 bundle 參數(shù)傳 nil 和傳 NSBundle.mainBundle() 是一樣的,就像蘋果文檔中說(shuō)的:
bundle 中包含了 storyboard 文件和相關(guān)的資源文件,如果你傳 nil,這個(gè)方法會(huì)去當(dāng)前應(yīng)用的 main bundle 中查找。
—— UIStoryboard Class Reference
和創(chuàng)建便利構(gòu)造方法等價(jià)的是創(chuàng)建一個(gè) UIStoryboard 類方法,該類方法返回 UIStoryboard 實(shí)例。
class func storyboard(storyboard: Storyboard, bundle: NSBundle? = nil) -> UIStoryboard {
return UIStoryboard(name: storyboard.rawValue, bundle: bundle)
}
...
let storyboard = UIStoryboard.storyboard(.News)
無(wú)論是創(chuàng)建便利構(gòu)造方法還是類方法,結(jié)果都是一樣的。唯一的差別是語(yǔ)法形式上的個(gè)人喜好,我個(gè)人認(rèn)為類方法更好一些,因此我會(huì)在自己的代碼中使用它們。無(wú)論你選擇哪種方式,確保在你的工程中保持一致就可以了。
好的,讓我們加大馬力來(lái)看看在文章開頭中吸引你的那些東西。
協(xié)議擴(kuò)展和泛型
通常工程中不會(huì)有那么多的 storyboard 文件,即使我們有 20 個(gè) storyboard 文件,我們也可以使用上面的方法來(lái)很好的維護(hù)它們。另一方面,控制器完全就是另一回事了。在我工作的 Xcode 工程中快速的搜索一下,我發(fā)現(xiàn)目前使用了超過(guò) 100 個(gè)不同的 UIViewController 子類。這是一個(gè)難題。
let storyboard = UIStoryboard.storyboard(.News)
let identifier = "ArticleViewController"
let viewController = storyboard.instantiateViewControllerWithIdentifier(identifier) as! ArticleViewController
現(xiàn)在我們不僅要管理代碼中的 storyboard 標(biāo)識(shí)符和 Interface Builder,還要處理各種各樣的類型轉(zhuǎn)換,因?yàn)檫@個(gè)方法只返回 UIViewController:
func instantiateViewControllerWithIdentifier(_ identifier: String) -> UIViewController
由于我們有如此多的 UIViewController 子類,所以之前在 UIStoryboard 中使用的枚舉方式會(huì)比字符串標(biāo)識(shí)符更好一些,但是這種方式管理這么多控制器仍顯笨拙。
StoryboardIdentifiable 協(xié)議
protocol StoryboardIdentifiable {
static var storyboardIdentifier: String { get }
}
我們創(chuàng)建一個(gè)任何類都可以遵循的協(xié)議,協(xié)議中有一個(gè)靜態(tài)變量 storyboardIdentifier。這將會(huì)減少我們管理控制器標(biāo)識(shí)符的工作量。
StoryboardIdentifiable 協(xié)議擴(kuò)展
extension StoryboardIdentifiable where Self: UIViewController {
static var storyboardIdentifier: String {
return String(self)
}
}
在我們的協(xié)議擴(kuò)展聲明中,where 子句表示該擴(kuò)展只適用于 UIViewController 或者它的子類。像 NSDate 這樣的類就不會(huì)獲取到 storyboardIdentifier 協(xié)議變量。
在協(xié)議擴(kuò)展中,我們提供了一個(gè)在運(yùn)行時(shí)動(dòng)態(tài)獲取 storyboardIdentifier 字符串的方法。
我最近才發(fā)現(xiàn) Swift 字符串有這樣的功能,這要感謝 NatashaTheRobot 的 文章。這個(gè)比 Objective-C 的 NSStringFromClass() 更好,這里是原因。(譯者注:同時(shí),翻譯組也翻譯了 Natasha 的這篇文章,詳見:《優(yōu)雅的 NSStringFromClass 替代方案》)
let classString = String(ArticleViewController)
print(classString)
// prints: ArticleViewController
StoryboardIdentifiable 全局一致性
extension UIViewController : StoryboardIdentifiable { }
現(xiàn)在我們讓工程中的每個(gè) UIViewController 都遵循 StoryboardIdentifiable 協(xié)議。這種方式減輕了工作量,使得我們不用更新每個(gè) UIViewController 來(lái)遵循該協(xié)議,同時(shí)也不需要記住在創(chuàng)建新的 UIViewController 類時(shí)讓它遵循該協(xié)議。
class ArticleViewController : UIViewController { }
...
print(ArticleViewController.storyboardIdentifier)
// prints: ArticleViewController
帶有泛型的 UIStoryboard 擴(kuò)展
func instantiateViewController<T: UIViewController where T: StoryboardIdentifiable>() -> T
我們擺脫了使用 storyboard 字符串標(biāo)識(shí)符從 storyboard 中創(chuàng)建控制器,取而代之的是一種更新更安全的方式:
extension UIStoryboard {
func instantiateViewController<T: UIViewController where T: StoryboardIdentifiable>() -> T {
let optionalViewController = self.instantiateViewControllerWithIdentifier(T.storyboardIdentifier)
guard let viewController = optionalViewController as? T else {
fatalError(“Couldn’t instantiate view controller with identifier \(T.storyboardIdentifier) “)
}
return viewController
}
}
這里我們使用泛型,它只允許我們傳入的類是 UIViewController 或者是 UIViewController 的子類,而且在泛型聲明中有一個(gè) where 子句,它限制了這些類也需要遵循 StoryboardIdentifiable 協(xié)議。
如果我們嘗試傳入一個(gè) NSObject 對(duì)象,Xcode 會(huì)編譯不過(guò)?;蛘呶覀儌魅胍粋€(gè) UIViewController 但是不遵循 StoryboardIdentifiable 協(xié)議的對(duì)象,Xcode 也不會(huì)編譯通過(guò)。這已經(jīng)足夠安全。
<T: UIViewController where T: StoryboardIdentifiable>() -> T
Yo! 這些奇怪的語(yǔ)法是什么?
通常泛型使用 “T” 作為參數(shù)名稱,然而你可以在尖括號(hào)里的第一次聲明時(shí)替換成任何你想要的名稱。如果我們想換,我們可以將 T 重命名為一個(gè)更易讀的名稱 “VC” 或者 “ViewController”:
<VC: UIViewController where VC: StoryboardIdentifiable>() -> VC
無(wú)論你使用哪個(gè)名稱,必須在聲明和方法體中保持一致。但是對(duì)于這個(gè)例子,我們會(huì)堅(jiān)持用 T,因?yàn)槟銜?huì)在其他的代碼和例子中發(fā)現(xiàn)這是 Swift 的傳統(tǒng)。
關(guān)于 Swift 泛型的更多內(nèi)容可以到官方文檔中查看
回到剛剛打斷的地方:
let optionalViewController = self.instantiateViewControllerWithIdentifier(T.storyboardIdentifier)
我們調(diào)用原始的 UIStoryboard 的 instantiateViewControllerWithIdentifier 方法,并傳遞 storyboardIdentifier 變量作為參數(shù),方法返回的是一個(gè)可選類型的 UIViewController。
guard let
viewController = optionalViewController as? T
else {
fatalError(“Couldn’t instantiate view controller with identifier \(T.storyboardIdentifier) “)
}
return viewController
我們嘗試對(duì)可選類型的 UIViewController 對(duì)象進(jìn)行解包,并轉(zhuǎn)換成傳入的類型。如果由于某種原因控制器不存在,fatalError 方法會(huì)被調(diào)用,同時(shí)控制臺(tái)會(huì)在調(diào)試模式時(shí)通知你,因此這些錯(cuò)誤不會(huì)在發(fā)布版本中發(fā)生。
最后,我們返回類型是 T 的解包過(guò)的 viewController。
實(shí)踐
class ArticleViewController : UIViewController
{
func printHeadline() { }
}
...
let storyboard = UIStoryboard.storyboard(.News)
let viewController: ArticleViewController = storyboard.instantiateViewController()
viewController.printHeadline()
presentViewController(viewController, animated: true, completion: nil)
這就是全部,我們擺脫了丑陋的,不安全的字符串標(biāo)識(shí)符,取而代之的是枚舉、協(xié)議擴(kuò)展和泛型。
而且,我們可以通過(guò) UIStoryboard 方法實(shí)例化一個(gè)特殊類型的控制器對(duì)象,并且不需要類型轉(zhuǎn)換就可以執(zhí)行特殊的操作。這難道不是你一天當(dāng)中看到的最棒的事情嗎?
更新
感謝 Raifura Andrei 和 Kyle Davis 的反饋,我已經(jīng)更新了文章和示例代碼,簡(jiǎn)化了語(yǔ)法同時(shí)提高了可讀性。Github 和 Gists 也同步更新了。享受吧。
本文的示例代碼可以在 Github 上找到。
本文由 SwiftGG 翻譯組翻譯,已經(jīng)獲得作者翻譯授權(quán),最新文章請(qǐng)?jiān)L問 http://swift.gg。