作者:Natasha The Robot,原文鏈接,原文日期:2016-05-13
譯者:Lanford3_3;校對(duì):numbbbbb寨典;定稿:Channe
和我一起參加9 月 1 日 - 9月 2 日在紐約舉辦的 Swift 社區(qū)慶典??吧!使用優(yōu)惠碼 NATASHATHEROBOT 可以獲得 $100 的折扣房匆!
我最近做了個(gè) Swift 面向協(xié)議編程實(shí)踐(POP??) 的演講耸成。視頻還在處理中。另一方面浴鸿,這是演講中 POP 視圖部分的文本記錄井氢,供我和其他任何人作參考!
簡(jiǎn)單的任務(wù)
假設(shè)你要寫一個(gè)由一張圖片和一個(gè)按鈕構(gòu)成的簡(jiǎn)單應(yīng)用岳链,產(chǎn)品經(jīng)理希望按鈕被點(diǎn)擊的時(shí)候圖片會(huì)抖動(dòng)毙沾,就像這樣:
由于這個(gè)動(dòng)畫常常在用戶名或者密碼輸入錯(cuò)誤時(shí)被用到,所以我們很容易就能在 StackOverflow 上找到代碼(就像每個(gè)好的開發(fā)者都會(huì)做的一樣??)
這個(gè)需求最難的地方就是決定實(shí)現(xiàn)抖動(dòng)的代碼應(yīng)該寫在哪兒宠页,但這其實(shí)也沒(méi)多難左胞。我寫了個(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()
}
}
這并沒(méi)什么令人激動(dòng)的俭嘁。任務(wù)完成躺枕,現(xiàn)在我可以繼續(xù)處理別的任務(wù)了……感謝 StackOverflow!
功能拓展
然而,就像實(shí)際開發(fā)中會(huì)發(fā)生的那樣供填,當(dāng)你認(rèn)為你搞定了任務(wù)拐云,可以繼續(xù)下一項(xiàng)的時(shí)候,設(shè)計(jì)師跳了出來(lái)告訴你他們希望按鈕能夠和 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()
}
}
但愿你沒(méi)這么做……在兩個(gè)地方重復(fù)編寫 shake() 方法違背了 DRY(don't repeat yourself)原則薇缅。如果之后一個(gè)設(shè)計(jì)師又過(guò)來(lái)表示需要更多或者更少的視圖進(jìn)行抖動(dòng),你就不得不在多處修改邏輯攒磨,這樣當(dāng)然并不理想泳桦。
所以該如何重構(gòu)呢?
通常的處理方式
如果你寫過(guò) 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 的代碼中并沒(méi)有什么特別的東西表示它們能夠抖動(dòng)。只是因?yàn)槟銓懥四莻€(gè)拓展(或分類)浮毯,你知道有那么一個(gè)能實(shí)現(xiàn)抖動(dòng)的方法被放在其中某處完疫。
再進(jìn)一步說(shuō),這種分類模式很容易就會(huì)失控亲轨。分類容易變成一個(gè)垃圾桶趋惨,以存放那些你不知道該放到哪里的代碼鸟顺。很快惦蚊,分類里的東西就太多了,你甚至都不知道一些代碼為什么在那兒讯嫂,又該用在哪兒蹦锋。你可以從 為什么分類被認(rèn)為是不好的 中了解更多。
所以欧芽,該怎么做呢……??
用協(xié)議(Protocol)來(lái)搞定莉掂!
你猜對(duì)了!Swifty 的解決方案就是用協(xié)議千扔!我們能夠利用協(xié)議拓展的力量來(lái)創(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)是可讀性曲楚!僅僅通過(guò) FoodImageView 和 ActionButton 的類聲明厘唾,你就能立刻知道它能抖動(dòng)。
如果設(shè)計(jì)師跑過(guò)來(lái)表示希望在抖動(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)起來(lái)也超級(jí)簡(jiǎn)單。只要移除對(duì) Shakeable 協(xié)議的遵從就好了逊朽!
class FoodImageView: UIImageView, Dimmable {
// 其他實(shí)現(xiàn)寫在這兒
}
結(jié)論
使用協(xié)議拓展來(lái)構(gòu)造視圖, 你就為你的代碼庫(kù)增加了超級(jí)棒的可讀性罕伯,復(fù)用性和可維護(hù)性
P.S. 我推薦閱讀 透明視圖控制器及背景遮罩 教程以了解更多這種模式的高級(jí)應(yīng)用。
譯者注叽讳,原文評(píng)論中有人認(rèn)為 “面向協(xié)議的視圖” 并沒(méi)必要捣炬,增加了過(guò)多的代碼(每個(gè)功能都要寫個(gè)協(xié)議)及不必要的代碼層次(分類/拓展的話是 類 -> 方法,而協(xié)議是 類 -> 協(xié)議 -> 方法)绽榛,一般的需求沒(méi)必要這樣湿酸,并提供了一個(gè)演講供參考,演講大意是避免不必要的層層封裝灭美,保持簡(jiǎn)單實(shí)現(xiàn)推溃,代碼的未來(lái)的拓展什么的自然有維護(hù)團(tuán)隊(duì)(=,=届腐?)做等等铁坎。另外也有其他讀者對(duì)之進(jìn)行了反駁蜂奸,感興趣可以看看。個(gè)人還是支持作者的觀點(diǎn)硬萍。
本文由 SwiftGG 翻譯組翻譯扩所,已經(jīng)獲得作者翻譯授權(quán),最新文章請(qǐng)?jiān)L問(wèn) http://swift.gg朴乖。