用 Swift 編寫面向協(xié)議的視圖

作者: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)毙沾,就像這樣:

1.gif

由于這個(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)……

2.gif

當(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ò) FoodImageViewActionButton 的類聲明厘唾,你就能立刻知道它能抖動(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朴乖。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末祖屏,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子买羞,更是在濱河造成了極大的恐慌袁勺,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件畜普,死亡現(xiàn)場(chǎng)離奇詭異期丰,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)吃挑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門钝荡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人舶衬,你說(shuō)我怎么就攤上這事埠通。” “怎么了约炎?”我有些...
    開封第一講書人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵植阴,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我圾浅,道長(zhǎng)掠手,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任狸捕,我火速辦了婚禮喷鸽,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘灸拍。我一直安慰自己做祝,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開白布鸡岗。 她就那樣靜靜地躺著混槐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪轩性。 梳的紋絲不亂的頭發(fā)上声登,一...
    開封第一講書人閱讀 51,301評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼悯嗓。 笑死件舵,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的脯厨。 我是一名探鬼主播铅祸,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼合武!你這毒婦竟也來(lái)了临梗?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤眯杏,失蹤者是張志新(化名)和其女友劉穎夜焦,沒(méi)想到半個(gè)月后壳澳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體岂贩,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年巷波,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了萎津。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡抹镊,死狀恐怖锉屈,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情垮耳,我是刑警寧澤颈渊,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站终佛,受9級(jí)特大地震影響俊嗽,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜铃彰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一绍豁、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧牙捉,春花似錦竹揍、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至带到,卻和暖如春昧碉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工晌纫, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留税迷,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓锹漱,卻偏偏與公主長(zhǎng)得像箭养,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子哥牍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容