你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 【譯】使用UIKit進行面向對象的編程

【譯】使用UIKit進行面向對象的編程

編輯:IOS開發基礎

learn.jpg

本文由CocoaChina譯者Leon(社區ID)翻譯
原文:iOS 9 Tutorial Series: Protocol-Oriented Programming with UIKit
轉載請保持所有內容和鏈接的完整性。


在WWDC 2015上,Apple談了Swift中面向協議編程的話題,令人深思。在那之後,好像每個人都在討論關於協議擴展的話題,這個新的語言特性使每個人都有所困惑。

我閱讀了許多關於Swift中協議的文章,了解過了協議擴展(protocol extensions)的詳情。毫無疑問,協議擴展將是Swift這道菜中的一位重要調料。Apple甚至建議盡可能的使用協議(protocol)來替換類(class)--這是面向協議編程的關鍵。

我讀過許多文章,其中對協議擴展的定義講的很清晰。但都沒有說明面向協議編程真正能為UI開發帶來些什麼。當前可用的一些示例代碼並不是基於一些實際場景的,而且沒用應用任何框架。

我想要知道面向協議編程是如何影響已有的應用,以及該如何在一個最常用的iOS庫(例如UIKit)中最大化的發揮它的作用。

既然我們已經有了協議擴展,基於協議的方法是否在UIkit這個“類重地”上有更大的價值?這篇文章中我將嘗試在真實的UI使用場景中講述Swift協議擴展。通過研究的過程來說明協議擴展並不是我之前所想的樣子。

協議的好處

協議並不是什麼新事物,但使用內置功能、共享邏輯,甚至“魔法能力”來擴展協議的想法很迷人。更多的協議意味著更大的靈活性。協議擴展是模塊化功能的一部分,它可以被采用(adopted),被覆蓋(overriden),也可以通過where語句進行指定類型的訪問。

從編譯角度來說,協議本身只能迎合編譯器。但是協議擴展卻是實際的代碼塊,可以被整個代碼庫使用。

不同於從父類繼承子類,我們可以使用任意多個協議。使用擴展協議就像是在Angular.js中為一個元素添加一條指令--我們插入一段代碼邏輯來替換對象的行為。這裡,協議已經不單單是一種約定,通過擴展的方式我們可以使用實際的功能。

如何使用擴展協議

方法很簡單。本文不會介紹如何使用,而會討論在UIKit中的實際應用。如果你需要盡快了解協議是如何工作的,請參考:Official Swift Documentation on Procotol Extensions.

協議擴展的局限

開始之前,讓我們先搞清楚協議不能做什麼。許多協議不能做的事情是出於設計考慮。不過我也很希望看到Apple在未來的Swift版本中處理這些限制。

在Objective-C中不能調用擴展協議的成員。

  • 不能對struct類型使用where語句

  • 不能在一個if let語句中定義多個逗號分隔的where語句

  • 不能在協議擴展中存儲動態變量

   1.這條對非泛型擴展也同樣使用

   2.靜態變量理論上是支持的,但是在Xcode 7.0上使用會報錯:“static stored properties not yet supported in generic types”

  • 不能在擴展協議中調用super(這點不同於非泛型擴展) @ketzusaka

    基於這個原因,沒有真正意義上的協議擴展繼承。

  • 不能使用多個協議擴展中同名的成員。

   1.Swift運行時環境會選擇最後一個協議中的成員並且忽略其他的。

   2.例如:如果我們使用兩個擴展協議,其中實現了兩個同名方法,當調用該方法時,只有最後一個協議中的方法會被調用。其他擴展中的方法調用不到。

  • 不能擴展可選(optional)的協議方法。

   1.可選協議方法需要@objc的標記,這樣就無法同時使用協議擴展。

  • 無法同時聲明協議和它的擴展。

   1.最好聲明extension protocol SomeProtocol {},這樣就同時聲明了協議並且實現了擴展。

Part 1:擴展現有UIKit協議

剛開始研究協議擴展時,第一個想到的是UITableViewDataSource,它或許是iOS平台上使用最廣的協議。如果可以為UITableViewDataSource協議添加一個默認的實現,這不是很有意思嗎?

如果應用中每個UITableView都有固定的若干個section,為什麼不擴展UITableViewDataSource並且在其中實現numberOfSectionsInTableView: 方法?如果所有的table都有滑動刪除的功能,擴展UITableViewDelegate協議並實現相應方法就完美多了。

潑盆冷水吧,這些都是不可能的。

  • 不可能任務:

為Objective-C協議提供默認實現。

UIKit仍然使用Objective-C編譯,而Objective-C中並沒有協議擴展的概念。在實際使用中,這意味著即使我們可以聲明UIKit協議的擴展,對於UIKit對象來說,擴展協議中的方法仍然是不可見的。

例如:如果我們擴展UICollectionViewDelegate 並實現collectionView:didSelectItemAtIndexPath:方法。在我們點擊cell的時候,這個方法並不會被調用。因為UICollectionView在Objective-C上下文中查找不到這個擴展方法。如果我們把如collectionView:cellForItemAtIndexPath:此類必要(required)方法放在協議擴展中,編譯器還是會提示使用該協議的類沒有遵循UICollectionViewDelegate協議。

Xcode嘗試通過添加@objc標簽來解決這個問題,但是這是徒勞的,會有一個新的錯誤:"協議擴展中的方法不能用Objective-C實現"。這是個隱藏錯誤:協議擴展只能在Swift 2以上代碼中使用。

  • 我們能做的:

為現有的Objective-C協議添加新的方法

我們可以通過Swift直接調用UIKit協議的擴展方法,即使對於UIKit來說它們是不可見的。這意味著我們不能覆蓋已有的協議方法,但是可以為協議添加新的方法。

這並沒有什麼驚喜之處,因為Objective-C代碼依然不能訪問這些方法。但還是帶來了一些機會。以下是一些組合使用協議擴展和現有UIKit協議的可能方式。

UIKit協議擴展示例:

擴展UICoordinateSpace

你以前一定嘗試過UIKit和Core Graphics坐標之間的相互轉換(左上坐標系->左上坐標系)。我們可以為UICoordinateSpace(一個UIView使用的協議)添加一些便利方法。

extension UICoordinateSpace {
    func invertedRect(rect: CGRect) -> CGRect {
        var transform = CGAffineTransformMakeScale(1, -1)
        transform = CGAffineTransformTranslate(transform, 0, -self.bounds.size.height)
        return CGRectApplyAffineTransform(rect, transform)
    }
}

現在我們的invertedRect方法可以被所有使用UICoordinateSpace的對象調用。我們可以在繪制代碼中這樣使用:

class DrawingView : UIView {
    // Example -- Referencing custom UICoordinateSpace method inside UIView drawRect.
    override func drawRect(rect: CGRect) {
        let invertedRect = self.invertedRect(CGRectMake(50.0, 50.0, 200.0, 100.0))
        print(NSStringFromCGRect(invertedRect)) // 50.0, -150.0, 200.0, 100.0
    }
}

擴展UITableViewDataSource協議

雖然不能修改UITableViewDataSource 的默認實現,我們還是可以添加一些公用代碼到UITableViewDataSource 中。

extension UITableViewDataSource {
    // Returns the total # of rows in a table view.
    func totalRows(tableView: UITableView) -> Int {
        let totalSections = self.numberOfSectionsInTableView?(tableView) ?? 1
        var s = 0, t = 0
        while s < totalSections {
            t += self.tableView(tableView, numberOfRowsInSection: s)
            s++
        }
        return t
    }
}

totalRows:方法可以快速計算table view中所有條目的數量。如果有個label顯示條目數量,而我們的數據都分散在各個section中的時候,這個方法格外有用。比如在tableView:titleForFooterInSection:方法中:

class ItemsController: UITableViewController {
    // Example -- displaying total # of items as a footer label.
    override func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? {
        if section == self.numberOfSectionsInTableView(tableView)-1 {
            return String("Viewing %f Items", self.totalRows(tableView))
        }
        return ""
    }
}

擴展UIViewControllerContextTransitioning協議

如果讀過我針對iOS 7寫的文章 Custom Navigation Transitions & More,並使用其中的方法自定義navigation的過渡。以下就有一組我使用過的方法,通過擴展UIViewControllerContextTransitioning 協議來實現。

extension UIViewControllerContextTransitioning {
    // Mock the indicated view by replacing it with its own snapshot. Useful when we don't want to render a view's subviews during animation, such as when applying transforms.
    func mockViewWithKey(key: String) -> UIView? {
        if let view = self.viewForKey(key), container = self.containerView() {
            let snapshot = view.snapshotViewAfterScreenUpdates(false)
            snapshot.frame = view.frame
            
            container.insertSubview(snapshot, aboveSubview: view)
            view.removeFromSuperview()
            return snapshot
        }
        
        return nil
    }
    
    // Add a background to the container view. Useful for modal presentations, such as showing a partially translucent background behind our modal content.
    func addBackgroundView(color: UIColor) -> UIView? {
        if let container = self.containerView() {
            let bg = UIView(frame: container.bounds)
            bg.backgroundColor = color
            
            container.addSubview(bg)
            container.sendSubviewToBack(bg)
            return bg
        }
        return nil
    }
}

我們可以在傳遞到animation coordinator的transitionContext對象調用這些方法

class AnimationCoordinator : NSObject, UIViewControllerAnimatedTransitioning {    // Example -- using helper methods during a view controller transition.
    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {        // Add a background
        transitionContext.addBackgroundView(UIColor(white: 0.0, alpha: 0.5))        
        // Swap out the "from" view
        transitionContext.mockViewWithKey(UITransitionContextFromViewKey)        
        // Animate using awesome 3D animation...
    }
    
    func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {        return 5.0
    }
}

擴展UIScrollViewDelegate協議

假設我們有許多個UIPageControl實例,我們需要拷貝粘貼UIScrollViewDelegate中的實現。使用協議擴展的方法我們可以全局訪問這段代碼,只需要簡單的使用self調用。

extension UIScrollViewDelegate {
    // Convenience method to update a UIPageControl with the correct page.
    func updatePageControl(pageControl: UIPageControl, scrollView: UIScrollView) {
        pageControl.currentPage = lroundf(Float(scrollView.contentOffset.x / (scrollView.contentSize.width / CGFloat(pageControl.numberOfPages))));
    }
}

另外,如果我們在使用UICollectionViewController,就可以去掉scrollView參數:

extension UIScrollViewDelegate where Self: UICollectionViewController {
    func updatePageControl(pageControl: UIPageControl) {
        pageControl.currentPage = lroundf(Float(self.collectionView!.contentOffset.x / (self.collectionView!.contentSize.width / CGFloat(pageControl.numberOfPages))));
    }
}

// Example -- Page control updates from a UICollectionViewController using a protocol extension.
class PagedCollectionView : UICollectionViewController {
    let pageControl = UIPageControl()
    
    override func scrollViewDidScroll(scrollView: UIScrollView) {
        self.updatePageControl(self.pageControl)
    }
}

不得不承認,以上例子都有些牽強。這說明了擴展現有UIKit協議並沒有太大的空間,而其價值並不明顯。不過,我們還是希望探索如何利用UIKit的設計模式擴展自定義協議。

Part 2:擴展自定義協議

MVC中使用面向協議編程

iOS程序內部通常包含3個重要部分。通常被描述為MVC(Model-View-Controller)模式。在App中使用這種模式來計算數據並展示出來。

MVC_Diagram_0.png

下面的三個例子中,我將展示一些有協議擴展特色的面向協議設計模式,依次用到Model->Controller->View組件。

Model管理中的協議(M)

假設我們有一個音樂類應用,叫Pear Music,裡面用到的model對象有Artists,Albums, Songs 和Playlists。我們需要通過某種標識,從網絡端加載這些model對象。

設計協議時,最好從頂端的抽象開始。基本思路是:有一個遠程資源,可以通過一個API來創建。我們這樣來定義協議:

// Any entity which represents data which can be loaded from a remote source.
protocol RemoteResource {}

等等,這只是個空協議。RemoteResource並未被顯式的使用。我們並不是需要一個約定,而是需要一系列設計網絡請求的功能。這樣說來,它真正的價值在於擴展:

extension RemoteResource {
    func load(url: String, completion: ((success: Bool)->())?) {
        print("Performing request: ", url)
        
        let task = NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: url)!) { (data, response, error) -> Void in
            if let httpResponse = response as? NSHTTPURLResponse where error == nil && data != nil {
                print("Response Code: %d", httpResponse.statusCode)
                
                dataCache[url] = data
                if let c = completion {
                    c(success: true)
                }
            } else {
                print("Request Error")
                if let c = completion {
                    c(success: false)
                }
            }
        }
        task.resume()
    }
    
    func dataForURL(url: String) -> NSData? {
        // A real app would require a more robust caching solution.
        return dataCache[url]
    }
}

public var dataCache: [String : NSData] = [:]

現在我們的協議有了內置的功能,可以加載並獲取遠程數據。所有應用該協議的對象都可以直接訪問這些方法。

假定還有兩個API需要調用,一個從"api.pearmusic.com"返回JSON類型數據; 另外一個從"media.pearmusic.com"返回media數據.要處理這些,我們為RemoteResource 協議創建子協議:

protocol JSONResource : RemoteResource {
    var jsonHost: String { get }
    var jsonPath: String { get }
    func processJSON(success: Bool)
}

protocol MediaResource : RemoteResource {
    var mediaHost: String { get }
    var mediaPath: String { get }
}

接下來是子協議(擴展)的實現:

extension JSONResource {
    // Default host value for REST resources
    var jsonHost: String { return "api.pearmusic.com" }
    
    // Generate the fully qualified URL
    var jsonURL: String { return String(format: "http://%@%@", self.jsonHost, self.jsonPath) }
    
    // Main loading method.
    func loadJSON(completion: (()->())?) {
        self.load(self.jsonURL) { (success) -> () in
            // Call adopter to process the result
            self.processJSON(success)
            
            // Execute completion block on the main queue
            if let c = completion {
                dispatch_async(dispatch_get_main_queue(), c)
            }
        }
    }
}

我們提供了默認的host名稱、創建完整URL的方法,還有加載資源的方法。接下來需要協議的使用者提供正確的jsonPath。

MediaResource使用同樣的模式:

extension MediaResource {
    // Default host value for media resources
    var mediaHost: String { return "media.pearmusic.com" }
    
    // Generate the fully qualified URL
    var mediaURL: String { return String(format: "http://%@%@", self.mediaHost, self.mediaPath) }
    
    // Main loading method
    func loadMedia(completion: (()->())?) {
        self.load(self.mediaURL) { (success) -> () in
            // Execute completion block on the main queue
            if let c = completion {
                dispatch_async(dispatch_get_main_queue(), c)
            }
        }
    }
}

如你所見,以上實現都很類似。事實上,將以上子協議中的代碼提到RemoteResource中會更合理,這樣子協議只需要返回正確的host名稱即可。

一個麻煩之處在於:這些協議之間並不互斥。也就是說,我們可能需要一個對象既是JSONResource,同時又是MediaResource。記住之前我們說過的,協議本身是會覆蓋的。只有最後一個協議中的方法會被調用,除非我們使用不同的屬性或方法。

讓我們來專門說說數據訪問方法:

extension JSONResource {
    var jsonValue: [String : AnyObject]? {
        do {
            if let d = self.dataForURL(self.jsonURL), result = try NSJSONSerialization.JSONObjectWithData(d, options: NSJSONReadingOptions.MutableContainers) as? [String : AnyObject] {
                return result
            }
        } catch {}
        return nil
    }
}

extension MediaResource {
    var imageValue: UIImage? {
        if let d = self.dataForURL(self.mediaURL) {
            return UIImage(data: d)
        }
        return nil
    }
}

這是用來說明協議擴展內涵的一個典型例子。傳統意義上的協議像是在說:“我有這些功能,因此我承諾我是這種類型”。一個擴展協議會說:“因為我有這些功能,我能做這些特別的事情”。因為MediaResource有image數據的訪問權限,因此應用MediaResource協議的對象可以提供imageValue,而不管它本身是什麼類型的,也不需要考慮上下文環境。

之前提到我們可以通過已知的標識符加載model對象。因此我們創建一個描述唯一標識的協議:

protocol Unique {
    var id: String! { get set }
}

extension Unique where Self: NSObject {
    // Built-in init method from a protocol!
    init(id: String?) {
        self.init()
        if let identifier = id {
            self.id = identifier
        } else {
            self.id = NSUUID().UUIDString
        }
    }
}

// Bonus: Make sure Unique adopters are comparable.
func ==(lhs: Unique, rhs: Unique) -> Bool {
    return lhs.id == rhs.id
}
extension NSObjectProtocol where Self: Unique {
    func isEqual(object: AnyObject?) -> Bool {
        if let o = object as? Unique {
            return o.id == self.id
        }
        return false
    }
}

這段代碼中,我們還是需要依賴於協議采用者提供“id”屬性,因為在協議擴展中我們不能存儲屬性。另外需要注意的一點是:這裡用where Self:NSObject語句限定只有在類型為NSObject時才可使用該擴展。不這樣做的話,就沒辦法調用self.init()方法,因為根本沒有它的聲明。一個替代方案是在該協議中自己聲明init()方法,但是這樣做的話,協議的采用者就必須顯式的實現它。因為所有的model對象都是NSObject的子類,因此這並不是問題。

OK,現在我們有了一個獲取網絡資源的基本方案。下來我們來創建遵循這些協議的model類型。首先是Song model類:

class Song : NSObject, JSONResource, Unique {
    // MARK: - Metadata
    var title: String?
    var artist: String?
    var streamURL: String?
    var duration: NSNumber?
    var imageURL: String?
    
    // MARK: - Unique
    var id: String!
}

等一下,JSONResource的(擴展)實現在哪裡?

比起直接在類中實現JSONResource的方法,使用條件控制的協議擴展更方便。這樣使我們可以將所有基於RemoteResource的代碼邏輯整合在一起,便於調整。另外,也使model類的實現更加整潔。添加如下代碼到RemoteResource.swift文件:

extension JSONResource where Self: Song {
    var jsonPath: String { return String(format: "/songs/%@", self.id) }
    
    func processJSON(success: Bool) {
        if let json = self.jsonValue where success {
            self.title = json["title"] as? String ?? ""
            self.artist = json["artist"] as? String ?? ""
            self.streamURL = json["url"] as? String ?? ""
            self.duration = json["duration"] as? NSNumber ?? 0
        }
    }
}

將這些內容都和RemoteResource關聯在一個位置,在組織上有很多好處。在一個位置編寫協議的實現方法,這裡擴展的作用范圍是清晰的。當聲明一個協議,且需要擴展時,我建議將擴展寫在同一個文件中。

有了JSONResource和Unique協議擴展,我們加載Song對象的代碼會像這樣:

let s = Song(id: "abcd12345")
let artistLabel = UILabel()

s.loadJSON { (success) -> () in
  artistLabel.text = s.artist
}

Duang!我們的Song對象就成了元數據的一個包裝,它本該如此。我們的協議擴展是真正的幕後英雄。

以下是Playlist對象的一個例子,它同時遵循JSONResource和MediaResource協議。

class Playlist: NSObject, JSONResource, MediaResource, Unique {
    // MARK: - Metadata
    var title: String?
    var createdBy: String?
    var songs: [Song]?
    
    // MARK: - Unique
    var id: String!
}

extension JSONResource where Self: Playlist {
    var jsonPath: String { return String(format: "/playlists/%@", self.id) }
    
    func processJSON(success: Bool) {
        if let json = self.jsonValue where success {
            self.title = json["title"] as? String ?? ""
            self.createdBy = json["createdBy"] as? String ?? ""
            // etc...
        }
    }
}

在我們摸索著為Playlist實現MediaResource協議之前,先稍稍退一步。我們意識到media API只需要identifier,而不需要考慮協議應用者的類型。這意味著,只要知道了identifier,就可以創建出mediaPath。用where語句可使MediaResource更智能的處理Unique協議。

extension MediaResource where Self: Unique {
   var mediaPath: String { return String(format: "/images/%@", self.id) }
}

因為我們的Playlist類已經遵循了Unique協議,因此不需要顯式的處理,它就可以和MediaResource搭配使用。對於所有MediaResource的使用者來說(它們也必然適配於Unique協議)也是一樣的:只要對象的identifier對應media API中的一張圖片,就可以通過這種方式創建mediaPath。

以下是加載Playlist圖片的方法:

let p = Playlist(id: "abcd12345")
let playlistImageView = UIImageView(frame: CGRectMake(0.0, 0.0, 200.0, 200.0))

p.loadMedia { () -> () in
  playlistImageView.image = p.imageValue
}

現在,我們已經有了一種定義遠程資源的通用方式,對於程序中任何實體都使用,而不局限於這些model對象。我們可以通過簡單的方式擴展RemoteResource,使其支持各種REST操作,另外,也可以針對其他數據類型創建子協議。

處理數據格式化的協議(C)

上文中我們創建了一種加載model對象的方法,繼續下一步:我們需要格式化對象中的元數據,並協調的顯示出來。

Peer Music是一個大應用,其中有許多不同類型的model。每個model都可能在不同的地方顯示。例如:作為view controller的title時,我們可能只顯示“name”。而如果有更多顯示空間的話,如UITableViewCell中,則顯示為“name instrument”。空間再多點的話,還可以顯示為“name instrument bio”。

當然,在controllers中,cell中,或者label中實現這些格式化方法沒有問題。但是如果能夠提取出這部分代碼邏輯,給整個app使用,會大大減少維護成本。

我們也可以將字符串格式化的代碼放到model對象中,但這樣在顯示字符串的時候,就必須確定model的類型。

也可以在基類中實現某些便利方法,由各model子類提供各自的格式化方式。由於我們正在討論面向協議編程,這裡就考慮的更通用一些。

考慮一下這樣的需求:將某些實體按字符串方式展現出來。上面的方法就可以推廣使用。針對不同的UI場景,可以提供出不同長度的字符串。

// Any entity which can be represented as a string of varying lengths.
protocol StringRepresentable {
    var shortString: String { get }
    var mediumString: String { get }
    var longString: String { get }
}

// Bonus: Make sure StringRepresentable adopters are printed descriptively to the console.
extension NSObjectProtocol where Self: StringRepresentable {
    var description: String { return self.longString }
}

簡單吧。以下是model對象使用StringRepresentable的例子:

class Artist : NSObject, StringRepresentable {
    var name: String!
    var instrument: String!
    var bio: String!
}

class Album : NSObject, StringRepresentable {
    var title: String!
    var artist: Artist!
    var tracks: Int!
}

和實現RemoteResource的方式類似,我們也將所有格式化字符串的邏輯放到StringRepresentable.swift文件中(這裡同樣有協議的聲明)。

extension StringRepresentable where Self: Artist {
    var shortString: String { return self.name }
    var mediumString: String { return String(format: "%@ (%@)", self.name, self.instrument) }
    var longString: String { return String(format: "%@ (%@), %@", self.name, self.instrument, self.bio) }
}
extension StringRepresentable where Self: Album {
    var shortString: String { return self.title }
    var mediumString: String { return String(format: "%@ (%d Tracks)", self.title, self.tracks) }
    var longString: String { return String(format: "%@, an Album by %@ (%d Tracks)", self.title, self.artist.name, self.tracks) }
}

現在,所有格式化功能都搞定了,現在可以考慮將其作用到不同的UI場景中。基於通用考慮,我們的設計用於顯示所有StringRepresentable的應用者,只要給出containerSize和containerFont用來計算即可。

protocol StringDisplay {
  var containerSize: CGSize { get }
  var containerFont: UIFont { get }
  func assignString(str: String)
}

建議只將方法聲明放置到協議中,協議的應用者(adopter)會實現這些方法。而對協議擴展來說,我們會添加真正的實現代碼。displayStringValue: 方法會決定使用哪個字符串,它會用assignString:將該字符串傳遞出去,而assignString:方法可以由不同的類實現。

extension StringDisplay {
    func displayStringValue(obj: StringRepresentable) {
        // Determine the longest string which can fit within the containerSize, then assign it.
        if self.stringWithin(obj.longString) {
            self.assignString(obj.longString)
        } else if self.stringWithin(obj.mediumString) {
            self.assignString(obj.mediumString)
        } else {
            self.assignString(obj.shortString)
        }
    }
    
#pragma mark - Helper Methods
    
    func sizeWithString(str: String) -> CGSize {
        return (str as NSString).boundingRectWithSize(CGSizeMake(self.containerSize.width, .max),
            options: .UsesLineFragmentOrigin,
            attributes:  [NSFontAttributeName: self.containerFont],
            context: nil).size
    }
    
    private func stringWithin(str: String) -> Bool {
        return self.sizeWithString(str).height <= self.containerSize.height
    }
}

現在我們的model對象已經遵循了StringRepresentable協議,另外,我們還有了可以自動選擇字符串的協議。下面看看如何在UIKit中使用。

從最簡單的UILabel開始吧。傳統做法是:繼承UILabel類,應用協議,然後在需要使用StringRepresentable來顯示的時候調用這個自定義的UILabel。而更好的方案(假定我們不需要繼承),就是使用指定類型的擴展(當然這裡指定的是UILabel類),讓所有的UILabel類自動適應StringDisplay協議。

extension UILabel : StringDisplay {
    var containerSize: CGSize { return self.frame.size }
    var containerFont: UIFont { return self.font }
    func assignString(str: String) {
        self.text = str
    }
}

只需要這麼多代碼。對於其他的UIKit類,都可以這麼做。只需要返回StringDisplay協議需要的數據,剩下的全由它幫忙搞定。

extension UITableViewCell : StringDisplay {
    var containerSize: CGSize { return self.textLabel!.frame.size }
    var containerFont: UIFont { return self.textLabel!.font }
    func assignString(str: String) {
        self.textLabel!.text = str
    }
}

extension UIButton : StringDisplay {
    var containerSize: CGSize { return self.frame.size}
    var containerFont: UIFont { return self.titleLabel!.font }
    func assignString(str: String) {
        self.setTitle(str, forState: .Normal)
    }
}

extension UIViewController : StringDisplay {
    var containerSize: CGSize { return self.navigationController!.navigationBar.frame.size }
    var containerFont: UIFont { return UIFont(name: "HelveticaNeue-Medium", size: 34.0)! } // default UINavigationBar title font
    func assignString(str: String) {
        self.title = str
    }
}

使用起來效果如何?接下來我們聲明一個Artist,它也會用StringRepresentable協議。

let a = Artist()
a.name = "Bob Marley"
a.instrument = "Guitar / Vocals"
a.bio = "Every little thing's gonna be alright."

因為所有的UIButton被擴展為適配StringDisplay協議,我們可以直接調用UIButton對象的displayStringValue:方法。

let smallButton = UIButton(frame: CGRectMake(0.0, 0.0, 120.0, 40.0))
smallButton.displayStringValue(a)

print(smallButton.titleLabel!.text) // 'Bob Marley'

let mediumButton = UIButton(frame: CGRectMake(0.0, 0.0, 300.0, 40.0))
mediumButton.displayStringValue(a)

print(mediumButton.titleLabel!.text) // 'Bob Marley (Guitar / Vocals)'

現在button會根據frame的大小自動選擇title來顯示。

若我們點擊一個Album,進入AlbumDetailsViewController的頁面,協議可以幫助我們找到一個合適的字符串作為navigation的標題。有了StringDisplay協議,UINavigationBar在iPad上會顯示長標題,而在iPhone上顯示短標題。

class AlbumDetailsViewController : UIViewController {
    var album: Album!
    
    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        
        // Display the right string based on the nav bar width.
        self.displayStringValue(self.album)
    }
}

現在我們可以相信,格式化model的工作可以由協議擴展單獨完成,並且能夠根據不同的UI元素靈活顯示。這種模式可以在以後的model對象上重復使用,適應於不同的UI元素。因為協議的這種可擴展性,它甚至可以用在許多非UI環境中。

樣式中使用協議(V)

我們已經了解了如何在model類和格式化字符串中使用協議擴展,現在,讓我們看看單純的前段實例,看一下協議擴展是如何使UI開發更加快捷。

我們把協議看作是類似於css類的東西,使用協議來定義UIKit對象的樣式,之後,應用樣式協議的對象可以自動改變顯示外觀。

首先,我們定義一個基礎協議,用來表示樣式處理的實體,在其中聲明一個最終用於處理樣式的方法。

// Any entity which supports protocol-based styling.
protocol Styled {
  func updateStyles()
}

接下來,我們創建一些子協議,定義具體需要的樣式。

protocol BackgroundColor : Styled {
  var color: UIColor { get }
}

protocol FontWeight : Styled {
  var size: CGFloat { get }
  var bold: Bool { get }
}

這樣,協議使用者就不需要進行顯式調用。

接著,我們定義各種特定樣式,在協議擴展的實現中返回需要的值。

protocol BackgroundColor_Purple : BackgroundColor {}
extension BackgroundColor_Purple {
    var color: UIColor { return UIColor.purpleColor() }
}

protocol FontWeight_H1 : FontWeight {}
extension FontWeight_H1 {
    var size: CGFloat { return 24.0 }
    var bold: Bool { return true }
}

最後,只需要根據不同的UIKit對象類型,實現updateStyles即可。用指定類型的擴展讓所有UITableViewCell的實例都遵循Styled協議。

extension UITableViewCell : Styled {
    func updateStyles() {
        if let s = self as? BackgroundColor {
            self.backgroundColor = s.color
            self.textLabel?.textColor = .whiteColor()
        }
        
        if let s = self as? FontWeight {
            self.textLabel?.font = (s.bold) ? UIFont.boldSystemFontOfSize(s.size) : UIFont.systemFontOfSize(s.size)
        }
    }
}

為保證updateStyles被自動調用,我們在擴展中重寫awakeFromNib方法。這裡你可能有點疑問,實際上,重寫的awakeFromNib方法被插入到了繼承鏈中,就好像是繼承自UITableViewCell類本身。這樣,在UITableViewCell子類中調用super,就會直接調用到這個方法。

public override func awakeFromNib() {
     super.awakeFromNib()
     self.updateStyles()
  }
}

現在,我們創建子類,然後通過應用協議來加載需要的樣式:

class PurpleHeaderCell : UITableViewCell, BackgroundColor_Purple, FontWeight_H1 {}

我們已經為UIKit的元素創建了類似css的樣式聲明。使用協議擴展,甚至可以為UIKit添加如Bootstrap的功能。這種方案在不同的方面都可以有所作為,特別是當程序中的樣式動態成都高、顯示元素較多時,更能發揮價值。

假定我們程序中有20+的view controller,每個都使用了2-3中顯示樣式。之前的我們只能被迫創建基類或者寫一堆用來定義樣式的全局函數;現在只需要實現並使用樣式協議就可以了。

我們得到了什麼?

到此為止我們已經嘗試了不少東西,它們都很有趣。但是思考一下:我們到底能從協議和協議擴展中獲得什麼?有人會認為根本沒有必要創建協議。

面向協議編程並不能完美適配於所有UI場景。

通常,當添加共享代碼或通用方法時,協議和協議擴展好處頗多。而且,代碼的組織性和函數相比更好。

數據類型越多,協議越能發揮用武之地。在UI需要顯示多種信息格式時,使用協議會得心應手。但這並不意味著,我們要添加6種協議和一打協議擴展來創建一個顯示artist名稱的紫色背景cell。

讓我們來補充Pear Music軟件的使用場景,來看看面向協議編程是否真的物有所值。

添加復雜度

假定我們已經維護了Pear Music一段時間,這個軟件可以顯示albums、artists和songs,有著友好的界面。我們又有巧妙的協議和擴展來維持MVC的結構。現在Pear的CEO要求我們創建Pear Music的2.0版本。我們需要和一個叫Apple Music的軟件進行競爭。

我們需要一項酷炫的新功能來證明自己,經過研究,決定添加“長按預覽”功能。這項功能創意新穎、獨到。公司裡長的像Jony Ive的哥們已經坐在鏡頭前侃侃而談。讓我們趕緊開始干活,用面向協議編程的方法來搞定它。

創建Modal Page

流程如下:用戶長按artist,album,song或者playlist,這時一個模態窗口(modal view)在屏幕上顯示出來,從網絡上加載條目的圖片,並顯示其描述,就像Facebook的分享按鈕做的那樣。

我們先來創建一個UIViewController,它將用來做模態顯示。從一開始,我們就考慮讓初始化方式更加通用,只需要一些遵循StringRepresentable和MediaResource協議的對象。

class PreviewController: UIViewController {
    @IBOutlet weak var descriptionLabel: UILabel!
    @IBOutlet weak var imageView: UIImageView!
    
    // The main model object which we're displaying
    var modelObject: protocol!
    
    init(previewObject: protocol) {
        self.modelObject = previewObject
    
        super.init(nibName: "PreviewController", bundle: NSBundle.mainBundle())
    }
}

接下來我們使用內置的協議擴展方法來給descriptionLabel和imageView傳遞數據:

override func viewDidLoad() {
        super.viewDidLoad()
        
        // Apply string representations to our label. Will use the string which fits into our descLabel.
        self.descriptionLabel.displayStringValue(self.modelObject)
        
        // Load MediaResource image from the network if needed
        if self.modelObject.imageValue == nil {
            self.modelObject.loadMedia { () -> () in
                self.imageView.image = self.modelObject.imageValue
            }
        } else {
            self.imageView.image = self.modelObject.imageValue
        }
    }

最後,通過同樣的方法獲取metadata,就像我們在Facebook例子中做的那樣。

// Called when tapping the Facebook share button.
    @IBAction func tapShareButton(sender: UIButton) {
        if SLComposeViewController.isAvailableForServiceType(SLServiceTypeFacebook) {
            let vc = SLComposeViewController(forServiceType: SLServiceTypeFacebook)
            
            // Use StringRepresentable.shortString in the title
            let post = String(format: "Check out %@ on Pear Music 2.0!", self.modelObject.shortString)
            vc.setInitialText(post)
            
            // Use the MediaResource url to link to
            let url = String(self.modelObject.mediaURL)
            vc.addURL(NSURL(string: url))
            
            // Add the entity's image
            vc.addImage(self.modelObject.imageValue!);
            
            self.presentViewController(vc, animated: true, completion: nil)
        }
    }
}

通過協議,我們獲得了很多便利,如果沒有它們,我們需要根據不同的數據類型,分別創建PreviewController的初始化方法。通過基於協議的方式,既可以保證view controller的簡潔性,又可以保證其擴展性。

按照這種方式,PreviewController不用分別處理Artist,Album,Song,Playlist等不同的數據類型,變得更加簡潔和輕量級。它甚至不用些一行數據類型相關的代碼。

集成第三方代碼

以下是本教程中最後一個酷炫的示例。同樣,用PreviewController展示。這裡我們需要集成一個新的框架,來展示Twitter上音樂家的信息。在主頁面上顯示推文列表,有一下的model類可以使用:

class TweetObject {
  var favorite_count: Int!
  var retweet_count: Int!
  var text: String!
  var user_name: String!
  var profile_image_id: String!
}

我們沒有這個框架的代碼,也無法修改TweetObject類,但是還是希望用戶能通過長按的方法在PreviewController的UI上顯示推文。這裡只需要通過應用現有協議來擴展它,就這麼簡單。

extension TweetObject : StringRepresentable, MediaResource {
    // MARK: - MediaResource
    var mediaHost: String { return "api.twitter.com" }
    var mediaPath: String { return String(format: "/images/%@", self.profile_image_id) }
    
    // MARK: - StringRepresentable
    var shortString: String { return self.user_name }
    var mediumString: String { return String(format: "%@ (%d Retweets)", self.user_name, self.retweet_count) }
    var longString: String { return String(format: "%@ Wrote: %@", self.user_name, self.text) }
}

這樣,我們就可以直接傳遞TweetObject的對象給PreviewController了。對於PreviewController來說,它甚至不需要知道現在正在和一個外部框架打交道。

let tweet = TweetObject()
let vc = PreviewController(previewObject: tweet)

課程總結

在WWDC2015上Apple建議創建協議,而不是類。但是我對這個觀點持懷疑態度,因為它忽略了在使用UIKit這個已類為重的框架時,協議擴展微妙的限制。只有當協議擴展被廣泛應用,而且不需要考慮舊代碼的時候,才能發揮它的威力。雖然在一開始我提到的例子看起來都很瑣碎,這種通用的設計在程序擴展、復雜度不斷提升時,還是非常有效。

在代碼解釋性和成本之間,需要綜合考慮。協議和擴展在大多數基於UI的程序中並不怎麼實用。如果你的app只有一個單view,顯示一種類型的數據,而且永遠不改變,就不用過分考慮實用協議。但是如果你的app要讓核心數據在不同的顯示狀態下切換,顯示樣式和展現方式多種多樣。這時,協議和協議擴展將成為數據和顯示層的橋梁,你會在後期使用中受益匪淺。

最後,我不想把協議看做是萬用靈藥,而是將其當做在某種開發場景中,一種創造性的工具。當然,我認為開發者嘗試一下面向協議技術是很有好處的,按照協議的方式,重新審視自己的代碼,你會發現很多不一樣的東西。聰明的使用它們。

如有問題,或需要更詳細的討論,請給我發郵件,或在Twitter上聯系我。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved