在WWDC 2015上,Apple談了Swift中面向協議編程的話題,使人深思。在那以後,好像每一個人都在討論關於協議擴展的話題,這個新的語言特性使每一個人都有所困惑。css
我閱讀了許多關於Swift中協議的文章,瞭解過了協議擴展(protocol extensions)的詳情。毫無疑問,協議擴展將是Swift這道菜中的一位重要調料。Apple甚至建議儘量的使用協議(protocol)來替換類(class)--這是面向協議編程的關鍵。html
我讀過許多文章,其中對協議擴展的定義講的很清晰。但都沒有說明面向協議編程真正能爲UI開發帶來些什麼。當前可用的一些示例代碼並非基於一些實際場景的,並且沒用應用任何框架。ios
我想要知道面向協議編程是如何影響已有的應用,以及該如何在一個最經常使用的iOS庫(例如UIKit)中最大化的發揮它的做用。編程
既然咱們已經有了協議擴展,基於協議的方法是否在UIkit這個「類重地」上有更大的價值?這篇文章中我將嘗試在真實的UI使用場景中講述Swift協議擴展。經過研究的過程來講明協議擴展並非我以前所想的樣子。json
協議的好處swift
協議並非什麼新事物,但使用內置功能、共享邏輯,甚至「魔法能力」來擴展協議的想法很迷人。更多的協議意味着更大的靈活性。協議擴展是模塊化功能的一部分,它能夠被採用(adopted),被覆蓋(overriden),也能夠經過where語句進行指定類型的訪問。設計模式
從編譯角度來講,協議自己只能迎合編譯器。可是協議擴展倒是實際的代碼塊,能夠被整個代碼庫使用。api
不一樣於從父類繼承子類,咱們可使用任意多個協議。使用擴展協議就像是在Angular.js中爲一個元素添加一條指令--咱們插入一段代碼邏輯來替換對象的行爲。這裏,協議已經不僅僅是一種約定,經過擴展的方式咱們可使用實際的功能。網絡
如何使用擴展協議app
方法很簡單。本文不會介紹如何使用,而會討論在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使用的協議)添加一些便利方法。
1
2
3
4
5
6
7
|
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的對象調用。咱們能夠在繪製代碼中這樣使用:
1
2
3
4
5
6
7
|
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 中。
1
2
3
4
5
6
7
8
9
10
11
12
|
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:方法中:
1
2
3
4
5
6
7
8
9
|
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 協議來實現。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
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對象調用這些方法
1
2
3
4
5
6
7
8
9
10
11
|
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調用。
1
2
3
4
5
6
|
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參數:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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中使用這種模式來計算數據並展現出來。
下面的三個例子中,我將展現一些有協議擴展特點的面向協議設計模式,依次用到Model->Controller->View組件。
Model管理中的協議(M)
假設咱們有一個音樂類應用,叫Pear Music,裏面用到的model對象有Artists,Albums, Songs 和Playlists。咱們須要經過某種標識,從網絡端加載這些model對象。
設計協議時,最好從頂端的抽象開始。基本思路是:有一個遠程資源,能夠經過一個API來建立。咱們這樣來定義協議:
1
2
|
// Any entity which represents data which can be loaded from a remote source.
protocol RemoteResource {}
|
等等,這只是個空協議。RemoteResource並未被顯式的使用。咱們並非須要一個約定,而是須要一系列設計網絡請求的功能。這樣說來,它真正的價值在於擴展:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
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 協議建立子協議:
1
2
3
4
5
6
7
8
9
10
|
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 }
}
|
接下來是子協議(擴展)的實現:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
extension JSONResource {
// Default host value for REST resources
var
jsonHost: String {
return
"api.pearmusic.com"
}
// Generate the fully qualified URL
// 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使用一樣的模式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
extension MediaResource {
// Default host value for media resources
var
mediaHost: String {
return
"media.pearmusic.com"
}
// Generate the fully qualified URL
// 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。記住以前咱們說過的,協議自己是會覆蓋的。只有最後一個協議中的方法會被調用,除非咱們使用不一樣的屬性或方法。
讓咱們來專門說說數據訪問方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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對象。所以咱們建立一個描述惟一標識的協議:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
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類:
1
2
3
4
5
6
7
8
9
10
11
|
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文件:
1
2
3
4
5
6
7
8
9
10
11
12
|
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對象的代碼會像這樣:
1
2
3
4
5
6
|
let s = Song(id:
"abcd12345"
)
let artistLabel = UILabel()
s.loadJSON { (success) -> ()
in
artistLabel.text = s.artist
}
|
Duang!咱們的Song對象就成了元數據的一個包裝,它本該如此。咱們的協議擴展是真正的幕後英雄。
如下是Playlist對象的一個例子,它同時遵循JSONResource和MediaResource協議。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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協議。
1
2
3
|
extension MediaResource where Self: Unique {
var
mediaPath: String {
return
String(format:
"/images/%@"
, self.id) }
}
|
由於咱們的Playlist類已經遵循了Unique協議,所以不須要顯式的處理,它就能夠和MediaResource搭配使用。對於全部MediaResource的使用者來講(它們也必然適配於Unique協議)也是同樣的:只要對象的identifier對應media API中的一張圖片,就能夠經過這種方式建立mediaPath。
如下是加載Playlist圖片的方法:
1
2
3
4
5
6
|
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場景,能夠提供出不一樣長度的字符串。
1
2
3
4
5
6
7
8
9
10
11
|
// 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的例子:
1
2
3
4
5
6
7
8
9
10
11
|
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文件中(這裏一樣有協議的聲明)。
1
2
3
4
5
6
7
8
9
10
|
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用來計算便可。
1
2
3
4
5
|
protocol StringDisplay {
var
containerSize: CGSize { get }
var
containerFont: UIFont { get }
func assignString(str: String)
}
|
建議只將方法聲明放置到協議中,協議的應用者(adopter)會實現這些方法。而對協議擴展來講,咱們會添加真正的實現代碼。displayStringValue: 方法會決定使用哪一個字符串,它會用assignString:將該字符串傳遞出去,而assignString:方法能夠由不一樣的類實現。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
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協議。
1
2
3
4
5
6
7
|
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協議須要的數據,剩下的全由它幫忙搞定。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
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協議。
1
2
3
4
|
let a = Artist()
a.name =
"Bob Marley"
a.instrument =
"Guitar / Vocals"
a.bio =
"Every little thing's gonna be alright."
|
由於全部的UIButton被擴展爲適配StringDisplay協議,咱們能夠直接調用UIButton對象的displayStringValue:方法。
1
2
3
4
5
6
7
8
9
|
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上顯示短標題。
1
2
3
4
5
6
7
8
9
10
|
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對象的樣式,以後,應用樣式協議的對象能夠自動改變顯示外觀。
首先,咱們定義一個基礎協議,用來表示樣式處理的實體,在其中聲明一個最終用於處理樣式的方法。
1
2
3
4
|
// Any entity which supports protocol-based styling.
protocol Styled {
func updateStyles()
}
|
接下來,咱們建立一些子協議,定義具體須要的樣式。
1
2
3
4
5
6
7
8
|
protocol BackgroundColor : Styled {
var
color: UIColor { get }
}
protocol FontWeight : Styled {
var
size: CGFloat { get }
var
bold: Bool { get }
}
|
這樣,協議使用者就不須要進行顯式調用。
接着,咱們定義各類特定樣式,在協議擴展的實現中返回須要的值。
1
2
3
4
5
6
7
8
9
10
|
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協議。
1
2
3
4
5
6
7
8
9
10
11
12
|
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,就會直接調用到這個方法。
1
2
3
4
5
|
public override func awakeFromNib() {
super
.awakeFromNib()
self.updateStyles()
}
}
|
如今,咱們建立子類,而後經過應用協議來加載須要的樣式:
1
|
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協議的對象。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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傳遞數據:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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例子中作的那樣。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// 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類可使用:
1
2
3
4
5
6
7
|
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上顯示推文。這裏只須要經過應用現有協議來擴展它,就這麼簡單。
1
2
3
4
5
6
7
8
9
10
|
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來講,它甚至不須要知道如今正在和一個外部框架打交道。
1
2
|
let tweet = TweetObject()
let vc = PreviewController(previewObject: tweet)
|
課程總結
在WWDC2015上Apple建議建立協議,而不是類。可是我對這個觀點持懷疑態度,由於它忽略了在使用UIKit這個已類爲重的框架時,協議擴展微妙的限制。只有當協議擴展被普遍應用,並且不須要考慮舊代碼的時候,才能發揮它的威力。雖然在一開始我提到的例子看起來都很瑣碎,這種通用的設計在程序擴展、複雜度不斷提高時,仍是很是有效。
在代碼解釋性和成本之間,須要綜合考慮。協議和擴展在大多數基於UI的程序中並不怎麼實用。若是你的app只有一個單view,顯示一種類型的數據,並且永遠不改變,就不用過度考慮實用協議。可是若是你的app要讓核心數據在不一樣的顯示狀態下切換,顯示樣式和展示方式多種多樣。這時,協議和協議擴展將成爲數據和顯示層的橋樑,你會在後期使用中受益不淺。
最後,我不想把協議看作是萬用靈藥,而是將其當作在某種開發場景中,一種創造性的工具。固然,我認爲開發者嘗試一下面向協議技術是頗有好處的,按照協議的方式,從新審視本身的代碼,你會發現不少不同的東西。聰明的使用它們。