對面向協(xié)議不熟悉的swift開發(fā)者,個人感覺這篇文章寫得很好,適合面向協(xié)議編程的初學(xué)者。
原文作者:http://www.tuicool.com/articles/AzAZvqQ
簡單的任務(wù)
假設(shè)你要寫一個由一張圖片和一個按鈕構(gòu)成的簡單應(yīng)用,產(chǎn)品經(jīng)理希望按鈕被點擊的時候圖片會抖動,就像這樣:
由于這個動畫常常在用戶名或者密碼輸入錯誤時被用到,所以我們很容易就能 在 StackOverflow 上找到代碼 (就像每個好的開發(fā)者都會做的一樣:grin:)
這個需求最難的地方就是決定實現(xiàn)抖動的代碼應(yīng)該寫在哪兒,但這其實也沒多難。我寫了個 UIImageView 的子類,再給它加上一個 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à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()
}
}
這并沒什么令人激動的。任務(wù)完成,現(xiàn)在我可以繼續(xù)處理別的任務(wù)了……感謝 StackOverflow!
功能拓展
然而,就像實際開發(fā)中會發(fā)生的那樣,當(dāng)你認為你搞定了任務(wù),可以繼續(xù)下一項的時候,設(shè)計師跳了出來告訴你他們希望按鈕能夠和 ImageView 一起抖動……
當(dāng)然,你可以重復(fù)上面的做法–寫個 UIButton 的子類,再加個 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)用戶點擊按鈕的時候,你就可以讓 ImageView 和按鈕一起抖動了:
// ViewController.swift
class ViewController: UIViewController {
@IBOutlet weak var foodImageView: FoodImageView!
@IBOutlet weak var actionButton: ActionButton!
@IBAction func onShakeButtonTap(sender: AnyObject) {
foodImageView.shake()
actionButton.shake()
}
}
但愿你沒這么做……在兩個地方重復(fù)編寫 shake() 方法違背了 DRY(don’t repeat yourself)原則。如果之后一個設(shè)計師又過來表示需要更多或者更少的視圖進行抖動,你就不得不在多處修改邏輯,這樣當(dāng)然并不理想。
所以該如何重構(gòu)呢?
通常的處理方式
如果你寫過 Objective-C, 你很可能會把 shake() 寫到一個 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()
}
}
然而,你立刻就會發(fā)現(xiàn),在 FoodImageView 或者 ActionButton 的代碼中并沒有什么特別的東西表示它們能夠抖動。只是因為你寫了那個拓展(或分類),你知道有那么一個能實現(xiàn)抖動的方法被放在其中某處。
再進一步說,這種分類模式很容易就會失控。分類容易變成一個垃圾桶,以存放那些你不知道該放到哪里的代碼。很快,分類里的東西就太多了,你甚至都不知道一些代碼為什么在那兒,又該用在哪兒?
所以,該怎么做呢……:thought_balloon:
用協(xié)議(Protocol)來搞定!
你猜對了!Swifty 的解決方案就是用協(xié)議!我們能夠利用協(xié)議拓展的力量來創(chuàng)建一個帶有默認 shake() 方法實現(xiàn)的 Shakeable 協(xié)議:
// Shakeable.swift
import UIKit
protocol Shakeable { }
// 你可以只為 UIView 添加 shake 方法!
extension Shakeable where Self: UIView {
// shake 方法的默認實現(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)在,我們只需要讓任何確實需要抖動的視圖遵從 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()
}
}
這里需要注意的第一點是可讀性!僅僅通過 FoodImageView 和 ActionButton 的類聲明,你就能立刻知道它能抖動。
如果設(shè)計師跑過來表示希望在抖動的同時 ImageView 能暗淡一點兒,我們也能夠利用相同的協(xié)議拓展模式添加新的功能,進行超級贊的功能組合。
// 添加暗淡功能
class FoodImageView: UIImageView, Shakeable, Dimmable {
// 其他實現(xiàn)寫在這兒
}
而且,當(dāng)產(chǎn)品經(jīng)理不再想讓 ImageView 抖動的時候,重構(gòu)起來也超級簡單。只要移除對 Shakeable 協(xié)議的遵從就好了!
class FoodImageView: UIImageView, Dimmable {
// 其他實現(xiàn)寫在這兒
}
結(jié)論
使用協(xié)議拓展來構(gòu)造視圖, 你就為你的代碼庫增加了超級棒的 可讀性 , 復(fù)用性 和 可維護性
譯者注,原文評論中有人認為 “面向協(xié)議的視圖” 并沒必要,增加了過多的代碼(每個功能都要寫個協(xié)議)及不必要的代碼層次(分類/拓展的話是 類 -> 方法,而協(xié)議是 類 -> 協(xié)議 -> 方法),一般的需求沒必要這樣,并提供了一個演講供參考,演講大意是避免不必要的層層封裝,保持簡單實現(xiàn),代碼的未來的拓展什么的自然有維護團隊(=,=?)做等等。另外也有其他讀者對之進行了反駁,感興趣可以看看。個人還是支持作者的觀點。
本文由 SwiftGG 翻譯組翻譯,已經(jīng)獲得作者翻譯授權(quán),最新文章請訪問http://swift.gg。