談談Swift面向協議編程
從一個具體需求說起
應用中有多個頁面內的 UICollectionViewCell 需要實現一個相同的小動畫:被選中時,先縮小到原來的0.8倍,再回彈到0.9倍。動畫本身實現起來不難:
func selectWithBounce(select:Bool, animated:Bool = true){
let bounce = CAKeyframeAnimation(keyPath: "transform")
let origin = CATransform3DIdentity
let smallest = CATransform3DMakeScale(0.8, 0.8, 1)
let small = CATransform3DMakeScale(0.9, 0.9, 1)
let originValue = NSValue(CATransform3D: origin)
let smallestValue = NSValue(CATransform3D:smallest)
let smallValue = NSValue(CATransform3D:small)
if animated {
bounce.duration = 0.2
bounce.removedOnCompletion = false
if select {
bounce.values = [originValue, smallestValue, smallValue]
self.layer.addAnimation(bounce, forKey: "bounce")
}else{
bounce.values = [smallestValue, originValue]
self.layer.addAnimation(bounce, forKey: "bounce")
}
}
if select {
self.layer.transform = small
}else{
self.layer.transform = origin
}
}</code></pre>
然而不同的頁面有不同的 UICollectionViewCell 子類,怎樣方便地讓它們都能復用這個動畫實現呢?
面向對象的復用方式
- 繼承
如果用面向對象的思維解決問題,最容易想到的就是定義個一個繼承自 UICollectionViewCell 的父類,實現這個動畫,然后所有需要這個動畫的cell都繼承它。
能解決問題,但缺點也很明顯:如果再來一個實現其他功能的方法需要復用,就沒有辦法了。Swift/Objective-C只能單繼承,如果把一段實現另一個功能的代碼也放到這個父類里,就引入了不必要的耦合。繼承一個類,就必須接受這個類的所有代碼,兩部分代碼沒有關系卻必須捆綁銷售,代碼就開始僵化了。
有的項目里定義了繼承 UIViewController 的父類,實現了很多功能,要求項目里所有的頁面都要繼承它。這種僵化的毛病就很明顯了,下面的子類代碼全都依賴這個父類,想抽出來復用非常難。
- 組合
組合優于繼承這是老生常談了,用在這里就要定義一個類專門負責做這個動畫,然后把 layer 作為方法參數或者成員變量傳進來。缺點嘛,就是有點麻煩。這種小工具類多了,要寫很多膠水代碼。
- Extension/Category
嚴格意義上講這個不能算典型的面向對象,而是Swift/Objective-C特有的功能。實現就不說了。缺點也是Extension/Category固有的,給一個類加上這東西會污染所有的對象,即使他們根本不需要這個功能。
面向協議的復用方式
協議擴展這個特性的引入,使得Swift支持了一種新的編程范式:面向協議編程。針對某個要實現的功能,我們可以使用協議定義出接口,然后利用協議擴展提供默認的實現。
上代碼:
protocol BounceSelect {
func selectWithBounce(select:Bool, animated:Bool)
}
extension BounceSelect where Self:UICollectionViewCell {
func selectWithBounce(select:Bool, animated:Bool = true){
let bounce = CAKeyframeAnimation(keyPath: "transform")
let origin = CATransform3DIdentity
let smallest = CATransform3DMakeScale(0.8, 0.8, 1)
let small = CATransform3DMakeScale(0.9, 0.9, 1)
let originValue = NSValue(CATransform3D: origin)
let smallestValue = NSValue(CATransform3D:smallest)
let smallValue = NSValue(CATransform3D:small)
if animated {
bounce.duration = 0.2
bounce.removedOnCompletion = false
if select {
bounce.values = [originValue, smallestValue, smallValue]
self.layer.addAnimation(bounce, forKey: "bounce")
}else{
bounce.values = [smallestValue, originValue]
self.layer.addAnimation(bounce, forKey: "bounce")
}
}
if select {
self.layer.transform = small
}else{
self.layer.transform = origin
}
}
}
這樣,一個 UICollectionViewCell 需要這個功能,只需要聲明遵守了這個協議即可,其他什么都不用做,直接就可以調用 func selectWithBounce(select:Bool, animated:Bool) 這個方法:
class XYZCollectionViewCell : UICollectionViewCell, BounceSelect {
...
}
let cell: XYZCollectionViewCell
cell.selectWithBounce(true)
遵守某個協議的類的對象調用協議聲明的方法時,如果類本身沒有提供實現,協議擴展提供的默認實現會被調用。
事實上,我們還可以進一步解除耦合。這個方法依賴的只有layer,不一定要是UICollectionViewCell。
另外我們可以允許用戶自定義動畫的時長,并提供默認的時長。
稍微改下代碼:
protocol BounceSelect {
var layer:CALayer {get}
var animationDuration:NSTimeInterval {get}
func selectWithBounce(select:Bool, animated:Bool)
}
extension BounceSelect {
var animationDuration:NSTimeInterval {
return 0.2
}
func selectWithBounce(select:Bool, animated:Bool = true){
let bounce = CAKeyframeAnimation(keyPath: "transform")
let origin = CATransform3DIdentity
let smallest = CATransform3DMakeScale(0.8, 0.8, 1)
let small = CATransform3DMakeScale(0.9, 0.9, 1)
let originValue = NSValue(CATransform3D: origin)
let smallestValue = NSValue(CATransform3D:smallest)
let smallValue = NSValue(CATransform3D:small)
if animated {
bounce.duration = animationDuration
bounce.removedOnCompletion = false
if select {
bounce.values = [originValue, smallestValue, smallValue]
self.layer.addAnimation(bounce, forKey: "bounce")
}else{
bounce.values = [smallestValue, originValue]
self.layer.addAnimation(bounce, forKey: "bounce")
}
}
if select {
self.layer.transform = small
}else{
self.layer.transform = origin
}
}
}
這個協議通過 var layer:CALayer {get} 定義了接入的條件,通過 func selectWithBounce(select:Bool, animated:Bool) 方法的聲明和實現,定義了它的功能。它就像一個插件,不管是 UIView 還是 NSView,只要提供一個 CALayer 屬性,都可以方便地接入這個功能。這樣就可以在不同的類之間復用這段代碼了,解除了對類型的依賴。這是繼承和
Extension/Category都無法做到的。
如果是CALayer本身呢?很簡單:
class XYZLayer : CALayer, BounceSelect {
//如果想修改動畫時長就重載這個屬性
var animationDuration:NSTimeInterval {
return 0.3
}
var layer:CALayer {
return self
}
}
面向協議編程的好處在于,通過協議+擴展實現一個功能,能夠定義所需要的 充分必要 條件,不多也不少。這樣就最大程度減少了耦合。使用者可以像搭積木一樣隨意組合這些協議,寫一個class或struct來完成復雜的功能。實際上,Swift的標準庫幾乎是everything is starting out as a protocol。
傳統的協議(比如Objective-C的protocol,Java的Interface)只能定義接口,不能復用實現,遵守同一個協議的不同的類,只能分別實現協議接口,使用場景受限了很多。Swift只是多了一個協議擴展的特性,但卻帶來了編程范式的進化。
來自:http://www.jianshu.com/p/4544b8837556