對(duì)面向協(xié)議不熟悉的swift開發(fā)者,個(gè)人感覺這篇文章寫得很好,適合面向協(xié)議編程的初學(xué)者。
原文作者:http://www.tuicool.com/articles/AzAZvqQ
簡單的任務(wù)
假設(shè)你要寫一個(gè)由一張圖片和一個(gè)按鈕構(gòu)成的簡單應(yīng)用,產(chǎn)品經(jīng)理希望按鈕被點(diǎn)擊的時(shí)候圖片會(huì)抖動(dòng),就像這樣:

由于這個(gè)動(dòng)畫常常在用戶名或者密碼輸入錯(cuò)誤時(shí)被用到,所以我們很容易就能 在 StackOverflow 上找到代碼 (就像每個(gè)好的開發(fā)者都會(huì)做的一樣:grin:)
這個(gè)需求最難的地方就是決定實(shí)現(xiàn)抖動(dòng)的代碼應(yīng)該寫在哪兒,但這其實(shí)也沒多難。我寫了個(gè) UIImageView 的子類,再給它加上一個(gè) shake() 方法就搞定了。
// FoodImageView.swift
import UIKit
class FoodImageView: UIImageView {
// shake() 方法寫在這兒
func shake() {
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.05
animation.repeatCount = 5
animation.autoreverses = true
animation.fromValue = NSValue(CGPoint: CGPointMake(self.center.x - 4.0, self.center.y))
animation.toValue = NSValue(CGPoint: CGPointMake(self.center.x + 4.0, self.center.y))
layer.addAnimation(animation, forKey: "position")
}
}
現(xiàn)在,當(dāng)用戶點(diǎn)擊按鈕的時(shí)候,我只要調(diào)用 ImageView 的 shake 方法就行了:
// ViewController.swift
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var foodImageView: FoodImageView!
@IBAction func onShakeButtonTap(sender: AnyObject) {
// 在這里調(diào)用 shake 方法
foodImageView.shake()
}
}
這并沒什么令人激動(dòng)的。任務(wù)完成,現(xiàn)在我可以繼續(xù)處理別的任務(wù)了……感謝 StackOverflow!
功能拓展
然而,就像實(shí)際開發(fā)中會(huì)發(fā)生的那樣,當(dāng)你認(rèn)為你搞定了任務(wù),可以繼續(xù)下一項(xiàng)的時(shí)候,設(shè)計(jì)師跳了出來告訴你他們希望按鈕能夠和 ImageView 一起抖動(dòng)……

當(dāng)然,你可以重復(fù)上面的做法–寫個(gè) UIButton 的子類,再加個(gè) shake 方法:
// ShakeableButton.swift
import UIKit
class ActionButton: UIButton {
func shake() {
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.05
animation.repeatCount = 5
animation.autoreverses = true
animation.fromValue = NSValue(CGPoint: CGPointMake(self.center.x - 4.0, self.center.y))
animation.toValue = NSValue(CGPoint: CGPointMake(self.center.x + 4.0, self.center.y))
layer.addAnimation(animation, forKey: "position")
}
}
現(xiàn)在,當(dāng)用戶點(diǎn)擊按鈕的時(shí)候,你就可以讓 ImageView 和按鈕一起抖動(dòng)了:
// ViewController.swift
class ViewController: UIViewController {
@IBOutlet weak var foodImageView: FoodImageView!
@IBOutlet weak var actionButton: ActionButton!
@IBAction func onShakeButtonTap(sender: AnyObject) {
foodImageView.shake()
actionButton.shake()
}
}
但愿你沒這么做……在兩個(gè)地方重復(fù)編寫 shake() 方法違背了 DRY(don’t repeat yourself)原則。如果之后一個(gè)設(shè)計(jì)師又過來表示需要更多或者更少的視圖進(jìn)行抖動(dòng),你就不得不在多處修改邏輯,這樣當(dāng)然并不理想。
所以該如何重構(gòu)呢?
通常的處理方式
如果你寫過 Objective-C, 你很可能會(huì)把 shake() 寫到一個(gè) UIView 的分類(Category) 中(也就是 Swift 中的拓展 (extension)):
// UIViewExtension.swift
import UIKit
extension UIView {
func shake() {
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.05
animation.repeatCount = 5
animation.autoreverses = true
animation.fromValue = NSValue(CGPoint: CGPointMake(self.center.x - 4.0, self.center.y))
animation.toValue = NSValue(CGPoint: CGPointMake(self.center.x + 4.0, self.center.y))
layer.addAnimation(animation, forKey: "position")
}
}
現(xiàn)在,UIImageView 和 UIButton(以及其他所有視圖)都有了可用的 shake() 方法:
class FoodImageView: UIImageView {
// 其他自定義寫在這兒
}
class ActionButton: UIButton {
// 其他自定義寫在這兒
}
class ViewController: UIViewController {
@IBOutlet weak var foodImageView: FoodImageView!
@IBOutlet weak var actionButton: ActionButton!
@IBAction func onShakeButtonTap(sender: AnyObject) {
foodImageView.shake()
actionButton.shake()
}
}
然而,你立刻就會(huì)發(fā)現(xiàn),在 FoodImageView 或者 ActionButton 的代碼中并沒有什么特別的東西表示它們能夠抖動(dòng)。只是因?yàn)槟銓懥四莻€(gè)拓展(或分類),你知道有那么一個(gè)能實(shí)現(xiàn)抖動(dòng)的方法被放在其中某處。
再進(jìn)一步說,這種分類模式很容易就會(huì)失控。分類容易變成一個(gè)垃圾桶,以存放那些你不知道該放到哪里的代碼。很快,分類里的東西就太多了,你甚至都不知道一些代碼為什么在那兒,又該用在哪兒?
所以,該怎么做呢……:thought_balloon:
用協(xié)議(Protocol)來搞定!
你猜對(duì)了!Swifty 的解決方案就是用協(xié)議!我們能夠利用協(xié)議拓展的力量來創(chuàng)建一個(gè)帶有默認(rèn) shake() 方法實(shí)現(xiàn)的 Shakeable 協(xié)議:
// Shakeable.swift
import UIKit
protocol Shakeable { }
// 你可以只為 UIView 添加 shake 方法!
extension Shakeable where Self: UIView {
// shake 方法的默認(rèn)實(shí)現(xiàn)
func shake() {
let animation = CABasicAnimation(keyPath: "position")
animation.duration = 0.05
animation.repeatCount = 5
animation.autoreverses = true
animation.fromValue = NSValue(CGPoint: CGPointMake(self.center.x - 4.0, self.center.y))
animation.toValue = NSValue(CGPoint: CGPointMake(self.center.x + 4.0, self.center.y))
layer.addAnimation(animation, forKey: "position")
}
}
現(xiàn)在,我們只需要讓任何確實(shí)需要抖動(dòng)的視圖遵從 Shakeable 協(xié)議就好了:
class FoodImageView: UIImageView, Shakeable {
// 其他自定義寫在這兒
}
class ActionButton: UIButton, Shakeable {
// 其他自定義寫在這兒
}
class ViewController: UIViewController {
@IBOutlet weak var foodImageView: FoodImageView!
@IBOutlet weak var actionButton: ActionButton!
@IBAction func onShakeButtonTap(sender: AnyObject) {
foodImageView.shake()
actionButton.shake()
}
}
這里需要注意的第一點(diǎn)是可讀性!僅僅通過 FoodImageView 和 ActionButton 的類聲明,你就能立刻知道它能抖動(dòng)。
如果設(shè)計(jì)師跑過來表示希望在抖動(dòng)的同時(shí) ImageView 能暗淡一點(diǎn)兒,我們也能夠利用相同的協(xié)議拓展模式添加新的功能,進(jìn)行超級(jí)贊的功能組合。
// 添加暗淡功能
class FoodImageView: UIImageView, Shakeable, Dimmable {
// 其他實(shí)現(xiàn)寫在這兒
}
而且,當(dāng)產(chǎn)品經(jīng)理不再想讓 ImageView 抖動(dòng)的時(shí)候,重構(gòu)起來也超級(jí)簡單。只要移除對(duì) Shakeable 協(xié)議的遵從就好了!
class FoodImageView: UIImageView, Dimmable {
// 其他實(shí)現(xiàn)寫在這兒
}
結(jié)論
使用協(xié)議拓展來構(gòu)造視圖, 你就為你的代碼庫增加了超級(jí)棒的 可讀性 , 復(fù)用性 和 可維護(hù)性
譯者注,原文評(píng)論中有人認(rèn)為 “面向協(xié)議的視圖” 并沒必要,增加了過多的代碼(每個(gè)功能都要寫個(gè)協(xié)議)及不必要的代碼層次(分類/拓展的話是 類 -> 方法,而協(xié)議是 類 -> 協(xié)議 -> 方法),一般的需求沒必要這樣,并提供了一個(gè)演講供參考,演講大意是避免不必要的層層封裝,保持簡單實(shí)現(xiàn),代碼的未來的拓展什么的自然有維護(hù)團(tuán)隊(duì)(=,=?)做等等。另外也有其他讀者對(duì)之進(jìn)行了反駁,感興趣可以看看。個(gè)人還是支持作者的觀點(diǎn)。
本文由 SwiftGG 翻譯組翻譯,已經(jīng)獲得作者翻譯授權(quán),最新文章請(qǐng)?jiān)L問http://swift.gg。