【译】使用UIKit进行面向对象的编程

在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
     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使用一样的模式:

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
     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。记住以前咱们说过的,协议自己是会覆盖的。只有最后一个协议中的方法会被调用,除非咱们使用不一样的属性或方法。

让咱们来专门说说数据访问方法:

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要让核心数据在不一样的显示状态下切换,显示样式和展示方式多种多样。这时,协议和协议扩展将成为数据和显示层的桥梁,你会在后期使用中受益不浅。

最后,我不想把协议看作是万用灵药,而是将其当作在某种开发场景中,一种创造性的工具。固然,我认为开发者尝试一下面向协议技术是颇有好处的,按照协议的方式,从新审视本身的代码,你会发现不少不同的东西。聪明的使用它们。

相关文章
相关标签/搜索