UIStoryboard:和枚舉、協議擴展、泛型一起使用更安全

mciy042 9年前發布 | 6K 次閱讀 泛型 iOS開發 移動開發 Storyboard

幾周前,我無意發現 Guille Gonzalez 寫的一篇 文章 ,介紹了如何用協議和擴展讓 UITableViewCell 的注冊和重用更安全。

看完這篇文章后我非常驚嘆,因為不需要依賴繼承,只需要協議擴展和泛型就可以非常容易的實現自定義的行為。自從 WWDC15,我們已經聽到關于 Swift 如何是一門 面向協議的語言 ,而我只是一知半解,如果你懂我的意思的話。而就在此時我終于明白他們在講的是什么了。

在我花費大量時間做的應用中有一個大的 storyboard,使用起來令人難以置信的繁瑣,所以我最后決定將它分離開。將一個巨大的 UIStoryboard 分成眾多小的 UIStoryboard,然后我只需要在我的代碼中用不同的字符串去實例化 UIStoryboard,但是這樣從來不安全。

字符串

你留在家中的無聲殺手

let name = "News"
let storyboard = UIStoryboard(name: name, bundle: nil)
let identifier = "ArticleViewController"
let viewController = storyboard.instantiateViewControllerWithIdentifier(identifier) as! ArticleViewController

上面的代碼中,我們創建了一個名稱為 “News” 的 UIStoryboard 實例,這會在工程資源目錄下找名稱為 “News.storyboard” 的文件。但是,如果有一個更復雜的名稱比如 “Onomatopoeia” ,由于這個詞非常奇怪并且不尋常,導致我為了文章的書寫必須查查怎么拼寫這個詞。可以想象,在工程代碼中如果我要不停的去猜如何拼寫類似的詞,這將是非常愚蠢的,但實際上人們就是在做著瘋狂的事情。

不知道怎么拼寫就算了,更糟的是由于它是一個字符串,Xcode 的語法檢查器并不會檢測出來拼寫錯誤,因此你只能在運行時才可能發現這種錯誤。唉!

如何才能讓 UIStoryboard 更安全

全局字符串常量

不,永遠不。剛開始這聽起來是一個好想法,因為你只需要定義一次常量就可以在任何地方使用。如果你想改變常量的值,只需要更改一處就可以讓工程中所有使用該常量的地方發生變化。

但是這樣你會少了一個變量名,你會對經常要重用一個變量名而感到驚訝。你曾經嘗試過給一個 NSObject 的子類的屬性命名為 “description” 嗎?這時你就知道我的意思了。如果 storyboards 使用多個字符串常量標識符的話,就會喪失一致性,如果它們定義在工程中的不同地方,也會使查找和合并它們變得更困難。

定義一個全局字符串常量還有諸多壞處,但是為了繼續討論文章中要提到的精華,所以我們將會忽略這些壞處。

關聯的 Storyboard 名稱

首要原則是你的 storyboard 應該以它包含的模塊命名。例如,如果一個 storyboard 包含的控制器是關于新聞的,那么就把 storyboard 文件命名為 “News.storyboard”。

統一 Storyboard 標識符

當你打算在你的控制器上使用 Storyboard 標識符時,通常的做法是使用類名作為標識符。比如 “ArticleViewController” 作為 ArticleViewController 的標識符。這將會減少你和同事們的負擔,你和你的同事們不必再去想和記憶統一標識符或者命名規范。

枚舉

可以考慮將枚舉作為統一的、中心全局化的 UIStoryboard 字符串標識符。為了讓 storyboard 實例化對象變得真正的安全,我們可以創建一個 UIStoryboard 類的擴展,其中定義了工程中不同的 storyboard 文件。

extension UIStoryboard {
    enum Storyboard : String {
        case Main
        case News
        case Gallery
    }
}

正如你所見,所有的內容在工程中都是統一并且中心化的。實例化也更加安全,當你敲擊標識符時 Xcode 也會自動補全。

let storyboard = UIStoryboard(name: UIStoryboard.Storyboard.News.rawValue, bundle: nil)

這段代碼可以順暢的編譯并運行,但是語法卻很丑陋。因此我們再深入地簡化一下語法,在 UIStoryboard 擴展中創建一個便利構造方法:

convenience init(storyboard: Storyboard, bundle: NSBundle? = nil) {
    self.init(name: storyboard.rawValue, bundle: bundle)
}
...
let storyboard = UIStoryboard(storyboard: .News)

你將會注意到, bundle: 參數默認是 nil ,因此在調用構造方法時可以忽略 bundle: 參數。

這樣做的原因是如果你傳 nil 給 bundle 參數,UIStroyboard 類會去 main bundle 中查找資源,所以給 bundle 參數傳 nil 和傳 NSBundle.mainBundle() 是一樣的,就像蘋果文檔中說的:

bundle 中包含了 storyboard 文件和相關的資源文件,如果你傳 nil,這個方法會去當前應用的 main bundle 中查找。

和創建便利構造方法等價的是創建一個 UIStoryboard 類方法,該類方法返回 UIStoryboard 實例。

class func storyboard(storyboard: Storyboard, bundle: NSBundle? = nil) -> UIStoryboard {
    return UIStoryboard(name: storyboard.rawValue, bundle: bundle)
}
...
let storyboard = UIStoryboard.storyboard(.News)

無論是創建便利構造方法還是類方法,結果都是一樣的。唯一的差別是語法形式上的個人喜好,我個人認為類方法更好一些,因此我會在自己的代碼中使用它們。無論你選擇哪種方式,確保在你的工程中保持一致就可以了。

好的,讓我們加大馬力來看看在文章開頭中吸引你的那些東西。

協議擴展和泛型

通常工程中不會有那么多的 storyboard 文件,即使我們有 20 個 storyboard 文件,我們也可以使用上面的方法來很好的維護它們。另一方面,控制器完全就是另一回事了。在我工作的 Xcode 工程中快速的搜索一下,我發現目前使用了超過 100 個不同的 UIViewController 子類。這是一個難題。

let storyboard = UIStoryboard.storyboard(.News)
let identifier = "ArticleViewController"
let viewController = storyboard.instantiateViewControllerWithIdentifier(identifier) as! ArticleViewController

現在我們不僅要管理代碼中的 storyboard 標識符和 Interface Builder,還要處理各種各樣的類型轉換,因為這個方法只返回 UIViewController:

func instantiateViewControllerWithIdentifier(_ identifier: String) -> UIViewController

由于我們有如此多的 UIViewController 子類,所以之前在 UIStoryboard 中使用的枚舉方式會比字符串標識符更好一些,但是這種方式管理這么多控制器仍顯笨拙。

StoryboardIdentifiable 協議

protocol StoryboardIdentifiable {
    static var storyboardIdentifier: String { get }
}

我們創建一個任何類都可以遵循的協議,協議中有一個靜態變量 storyboardIdentifier。這將會減少我們管理控制器標識符的工作量。

StoryboardIdentifiable 協議擴展

extension StoryboardIdentifiable where Self: UIViewController {
    static var storyboardIdentifier: String {
        return String(self)
    }
}

在我們的協議擴展聲明中, where 子句表示該擴展只適用于 UIViewController 或者它的子類。像 NSDate 這樣的類就不會獲取到 storyboardIdentifier 協議變量。

在協議擴展中,我們提供了一個在運行時動態獲取 storyboardIdentifier 字符串的方法。

我最近才發現 Swift 字符串有這樣的功能,這要感謝 NatashaTheRobot文章 。這個比 Objective-C 的 NSStringFromClass() 更好,這里是 原因 。(譯者注:同時,翻譯組也翻譯了 Natasha 的這篇文章,詳見: 《優雅的 NSStringFromClass 替代方案》

let classString = String(ArticleViewController)
print(classString) 
// prints: ArticleViewController

StoryboardIdentifiable 全局一致性

extension UIViewController : StoryboardIdentifiable { }

現在我們讓工程中的每個 UIViewController 都遵循 StoryboardIdentifiable 協議。這種方式減輕了工作量,使得我們不用更新每個 UIViewController 來遵循該協議,同時也不需要記住在創建新的 UIViewController 類時讓它遵循該協議。

class ArticleViewController : UIViewController { }
...
print(ArticleViewController.storyboardIdentifier)
// prints: ArticleViewController

帶有泛型的 UIStoryboard 擴展

func instantiateViewController<T: UIViewController where T: StoryboardIdentifiable>() -> T

我們擺脫了使用 storyboard 字符串標識符從 storyboard 中創建控制器,取而代之的是一種更新更安全的方式:

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 的子類,而且在泛型聲明中有一個 where 子句,它限制了這些類也需要遵循 StoryboardIdentifiable 協議。

如果我們嘗試傳入一個 NSObject 對象,Xcode 會編譯不過。或者我們傳入一個 UIViewController 但是不遵循 StoryboardIdentifiable 協議的對象,Xcode 也不會編譯通過。這已經足夠安全。

<T: UIViewController where T: StoryboardIdentifiable>() -> T

Yo! 這些奇怪的語法是什么?

通常泛型使用 “T” 作為參數名稱,然而你可以在尖括號里的第一次聲明時替換成任何你想要的名稱。如果我們想換,我們可以將 T 重命名為一個更易讀的名稱 “VC” 或者 “ViewController”:

<VC: UIViewController where VC: StoryboardIdentifiable>() -> VC

無論你使用哪個名稱,必須在聲明和方法體中保持一致。但是對于這個例子,我們會堅持用 T ,因為你會在其他的代碼和例子中發現這是 Swift 的傳統。

回到剛剛打斷的地方:

let optionalViewController = self.instantiateViewControllerWithIdentifier(T.storyboardIdentifier)

我們調用原始的 UIStoryboard 的 instantiateViewControllerWithIdentifier 方法,并傳遞 storyboardIdentifier 變量作為參數,方法返回的是一個可選類型的 UIViewController。

guard let 
    viewController = optionalViewController as? T 
else {
    fatalError(“Couldn’t instantiate view controller with identifier \(T.storyboardIdentifier) “)
}
return viewController

我們嘗試對可選類型的 UIViewController 對象進行解包,并轉換成傳入的類型。如果由于某種原因控制器不存在, fatalError 方法會被調用,同時控制臺會在調試模式時通知你,因此這些錯誤不會在發布版本中發生。

最后,我們返回類型是 T 的解包過的 viewController

實踐

class ArticleViewController : UIViewController
{ 
    func printHeadline() { }
}
...
let storyboard = UIStoryboard.storyboard(.News)
let viewController: ArticleViewController = storyboard.instantiateViewController()
viewController.printHeadline()
presentViewController(viewController, animated: true, completion: nil)

這就是全部,我們擺脫了丑陋的,不安全的字符串標識符,取而代之的是枚舉、協議擴展和泛型。

而且,我們可以通過 UIStoryboard 方法實例化一個特殊類型的控制器對象,并且不需要類型轉換就可以執行特殊的操作。這難道不是你一天當中看到的最棒的事情嗎?

 

 

 

來自:http://swift.gg/2016/09/26/uistoryboard-safer-with-enums-protocol-extensions-and-generics/

 

 本文由用戶 mciy042 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!