開(kāi)發(fā)中難免有需要制作復(fù)雜 UI 的 ViewController,子視圖一層套一層窃肠,夸張時(shí)層層視圖都有事件回調(diào),而我們只想在 “C” 中接收回調(diào)并處理,能讓代碼明明白白在業(yè)務(wù)處理層中展現(xiàn)削樊,讓場(chǎng)景易于維護(hù)、低耦合兔毒、迭代時(shí)清晰高效是我們所要達(dá)成的最終目的漫贞。
這篇文章主要是對(duì)結(jié)合《一種基于ResponderChain的對(duì)象交互方式》的方式與《關(guān)于使用 Swift 協(xié)議確保類(lèi)型的思考》的思考與實(shí)踐。文中涉及到有關(guān) Swift 消息派發(fā)機(jī)制知識(shí)育叁,推薦文章《深入理解 Swift 派發(fā)機(jī)制》
貫穿全文的例子
假設(shè)一個(gè)業(yè)務(wù)場(chǎng)景作為示例:做一個(gè)關(guān)于天空復(fù)雜視圖(View)
迅脐,天空中有一個(gè)太陽(yáng)(View)
,太陽(yáng)里有太陽(yáng)黑子(View)
豪嗽,這里點(diǎn)擊天空或太陽(yáng)谴蔑、太陽(yáng)的Subview都要直接在(ViewController)中做出相應(yīng)處理。
為了讓代碼簡(jiǎn)潔更好說(shuō)明龟梦,點(diǎn)擊事件使用簡(jiǎn)單的 touchesBegan(_:with:)
方法隐锭。
代理或閉包回調(diào)
閉包相比代理能少很多 XXXViewDelegate 及其實(shí)現(xiàn),但是從架構(gòu)上看计贰,closure 的處理代碼清晰程度必然比不上 delegate钦睡,因?yàn)閯?chuàng)建時(shí)機(jī)等關(guān)系,closure 的代碼可能散落各地躁倒,而 delegate 回調(diào)可以放在一目了然的位置荞怒,更加易于debug。
Delegates Closure 交互模式在層級(jí)單一樱溉,事件較少的業(yè)務(wù)邏輯里使用完全沒(méi)毛病挣输。然而在這個(gè)場(chǎng)景中,視圖越來(lái)越復(fù)雜后福贞,如例子中處理太陽(yáng)黑子的事件撩嚼,甚至點(diǎn)擊太陽(yáng)黑子時(shí)也要觸發(fā)點(diǎn)擊太陽(yáng)的事件就非常難受,我僅僅是想知道我點(diǎn)了一下太陽(yáng)黑子而已。層級(jí)增加隨之而來(lái)的問(wèn)題是代碼層級(jí)過(guò)多完丽,顯得臃腫維護(hù)吃力恋技。
// SunView.swift
protocol SunViewDelegate: class {
/// 從太陽(yáng)黑子 View 傳過(guò)來(lái)的,繼續(xù)向 上層傳遞
func sunView(_ view: SunView, didTapSunspot sunspotId: Int)
/// 點(diǎn)擊太陽(yáng) View
func sunViewDidTap(_ view: SunView)
}
class SunView: UIView {
weak var delegate: SunViewDelegate?
...
}
// SunspotView.swift
protocol SunspotViewDelegate: class {
/// 太陽(yáng)黑子點(diǎn)擊
func sunspotDidTap(_ view: SunspotView)
}
class SunspotView: UIView {
weak var delegate: SunspotViewDelegate?
...
}
用繼承抽象 Delegate
import UIKit
protocol SkyActionType {}
protocol SkyViewDelegate: class {
func homeSubview(_ view: UIView, didTap action: HomeActionType)
}
class SkyBaseView: UIView {
weak var delegate: HomeViewDelegate?
func updateView(model: Model) {
fatalError("[SkyBaseView] Unrealized")
}
}
// 使用
class SunView: SkyBaseView { override func updateView(model: Model) { ... } }
class SunspotView: SkyBaseView { override func updateView(model: Model) { ... } }
...
這是筆者以前使用過(guò)的一套模式逻族,這樣繼承存在很大問(wèn)題
- 1)所有子視圖都存在 Action
- 2)所有視圖都需要實(shí)現(xiàn) updateView
- 3)對(duì) Model 要求高
在功能迭代時(shí)蜻底,很難滿足上述幾點(diǎn),即便 updateView 默認(rèn)實(shí)現(xiàn)處不給激進(jìn)的 fatalError 錯(cuò)誤處理聘鳞,不能保證將來(lái)版本的迭代后的 View updateView
方法傳值還是 Model薄辅,要做額外泛型等處理,或是有的視圖根本不存在回調(diào)抠璃。
把 SkyViewController 的 Subviews 繼承一個(gè)基類(lèi)好處是可以減少代碼行數(shù)站楚,將事件封裝到一個(gè)代理中,便于修改與刪除原有邏輯搏嗡。但由于層級(jí)多窿春、隱藏關(guān)鍵代碼的緣故導(dǎo)致代碼可讀性差,過(guò)度依賴也讓代碼難以重用可維護(hù)性較差采盒,局限性大旧乞,即便完全理清代碼邏輯后,新增需求也因?yàn)闆](méi)有代碼提示的緣故容易漏寫(xiě)事件處理磅氨。復(fù)雜層級(jí)的視圖這么做也沒(méi)有解決層層回調(diào)代碼過(guò)與復(fù)雜的問(wèn)題尺栖。
我相信大家都碼出過(guò)這樣的代碼,雖然本意是想讓代碼更加優(yōu)美悍赢,但顯然還有更好的別的實(shí)現(xiàn)方式决瞳。
響應(yīng)鏈
如何汲取繼承的好處,將事件放在一塊兒維護(hù)左权?
ResponderChain 就是一個(gè)可以實(shí)現(xiàn)這樣需求的機(jī)制皮胡。雖說(shuō) UIResponder 也是逐級(jí)傳遞,但對(duì)開(kāi)發(fā)者來(lái)說(shuō)不必逐級(jí)實(shí)現(xiàn)傳遞功能赏迟。只需要 extension UIResponder
后添加一個(gè)傳遞方法屡贺。其原理不是文章主要內(nèi)容,不過(guò)多贅述锌杀,資料非常多甩栈。
圖中 SunspotView 發(fā)送事件(黑色箭頭),在響應(yīng)鏈后端 SunView 與 SkyView 都可以按需獲取 SunspotView 事件(灰色箭頭)來(lái)實(shí)現(xiàn)所需功能糕再。如果在 SunView 中不需要對(duì) SunspotView 的點(diǎn)擊事件做處理量没,開(kāi)發(fā)者則不需要考慮事件在 SunView 的傳遞實(shí)現(xiàn)。
Swift 代碼實(shí)現(xiàn):
enum SkyBehavior: String {
case clickSky = "clickSky"
case clickSun = "clickSun"
case clickSunspot = "clickSunspot"
}
extension UIResponder {
@objc func routerEvent(name: String, userInfo: [AnyHashable: Any]?) {
next?.routerEvent(name: name, userInfo: userInfo)
}
}
UIResponder 傳遞事件實(shí)現(xiàn)突想。
// SkyViewController.swift
class SkyViewController: UIViewController {
...
// 獲取所有子視圖事件
override func routerEvent(name: String, userInfo: [AnyHashable : Any]?) {
guard let event = SkyBehavior(rawValue: name) else { return }
switch event {
case .clickSky: print("[Sky] click sky")
case .clickSun: print("[Sky] click sun")
case .clickSunspot: print("[Sky] click sun spot")
}
}
}
在天空 VC SkyViewController 中處理事件并停止響應(yīng)鏈繼續(xù)傳遞殴蹄。
SkyViewController
× UIWindow
-> UIApplication
-> XCPAppDelegate
由于 Swift 中類(lèi)的拓展函數(shù)是被靜態(tài)派發(fā)的究抓,所以無(wú)法被子類(lèi)繼承,我們也不能將代碼直接添加到 UIResponder 的聲明域中用函數(shù)表來(lái)派發(fā)函數(shù)袭灯。解決辦法只能是利用 NSObject 拓展消息派發(fā)函數(shù)刺下,在 extension 中加 @objc dynamic
前綴。
如果在點(diǎn)擊太陽(yáng)黑子 SunspotView 的同時(shí)也觸發(fā)點(diǎn)擊太陽(yáng)的事件稽荧,只需要在 SunView 中重寫(xiě)方法橘茉,為了保證不在這里斷鏈,必須補(bǔ)上super.routerEvent(name: name, userInfo: userInfo)
姨丈。
class SunView: UIView {
...
override func routerEvent(name: String, userInfo: [AnyHashable : Any]?) {
super.routerEvent(name: name, userInfo: userInfo)
if name == SkyBehavior.clickSunspot.rawValue {
routerEvent(name: SkyBehavior.clickSun.rawValue, userInfo: nil)
}
}
}
代碼SkyExtensionResponderChainPlayground.swift
可以在這個(gè) gist 里找到畅卓。
ResponderChain 優(yōu)化
主要存在的問(wèn)題:
- 上述基于 ResponderChain 的交互用例功能被分發(fā)到了全局,所有繼承 UIResponder 的控件全部可以 router 局部的 SkyBehavior构挤。實(shí)際業(yè)務(wù)中髓介,現(xiàn)在 ResponderChain 的實(shí)現(xiàn)顯然是不合理,當(dāng)務(wù)之急是把發(fā)送事件的方法范圍縮小筋现,縮小到只有這個(gè)SkyViewController和與之相關(guān)的視圖才擁有這個(gè)功能。
- userInfo: [AnyHashable: Any]? 的類(lèi)型強(qiáng)制性不高箱歧,啥都可以往里面?zhèn)鞣桑?yàn)證功能時(shí)還得跑起來(lái)看回調(diào)是否到位。
使用泛型協(xié)議
喵神在文章開(kāi)頭提到里提到:「相比于 Objective-C 這類(lèi)“動(dòng)態(tài)”語(yǔ)言呀邢,Swift 在類(lèi)型安全上強(qiáng)制性要高出許多洒沦。配合上協(xié)議和 associatedtype,更是能做到另一個(gè)極致价淌,很多時(shí)候可以讓我們寫(xiě)出“無(wú)腦”的申眼,能通過(guò)編譯就不會(huì)有太大問(wèn)題的代碼〔跻拢」
Swift 是一門(mén)強(qiáng)類(lèi)型語(yǔ)言括尸,Swifter 更喜歡盡量把東西放在明面上,非工具類(lèi)業(yè)務(wù)少些動(dòng)態(tài)病毡,更愿意花時(shí)間做一勞永逸的事濒翻。Delegate 或 Closure 很大的優(yōu)點(diǎn)在于不論層級(jí)多么復(fù)雜,修改功能時(shí)會(huì)產(chǎn)生大量編譯錯(cuò)誤啦膜,等到開(kāi)發(fā)者處理完編譯錯(cuò)誤時(shí)有送,功能基本上也就修改完畢,而現(xiàn)在實(shí)現(xiàn)的 ResponderChain 并不擁有這個(gè)特性僧家,新增回調(diào)后忘了修改實(shí)現(xiàn)處代碼也能編譯通過(guò)雀摘,這只會(huì)增加 Debug 的時(shí)間。
實(shí)際業(yè)務(wù)中往往存在數(shù)據(jù)回調(diào)八拱,由于 router(event:)
函數(shù)是消息派發(fā)的阵赠,參數(shù)必須也是繼承 NSObject
的子類(lèi)涯塔,導(dǎo)致 SkyBehavior 枚舉不能用 Swift 的關(guān)聯(lián)值這個(gè)美妙的特性。利用泛型改造:
public protocol ResponderChainEventType {}
public protocol ResponderChainType {
func router<Event>(event: Event) where Event : ResponderChainEventType
}
extension ResponderChainType where Self: UIResponder {
public func router<Event>(event: Event) where Event : ResponderChainEventType {
// Responder handler
if let n = next as? SkyViewController {
n.router(event: event)
} else if let n = next as? SunView {
n.router(event: event)
}
// Other hander ...
else {
next?.router(event: event)
}
}
}
extension UIResponder: ResponderChainType {}
改造后 ResponderChainType 對(duì) router 泛型包裝豌注,在拓展中這樣一來(lái)伤塌,之前的func routerEvent(name: String, userInfo: [AnyHashable : Any]?)
直接改造成func router<Event>(event: Event) where Event : ResponderChainEventType
,實(shí)現(xiàn)枚舉回調(diào)事件轧铁,其中 ResponderChainEventType 是事件的泛型約束每聪,約束泛型 Event ,操作更加安全可靠齿风。
enum SkyBehavior: ResponderChainEventType {
case clickSky
case clickSun
case clickSunspot(id: Int)
}
// SkyViewController.swift
class SkyViewController: UIViewController {
...
func router<Event>(event: Event) where Event : ResponderChainEventType {
guard let e = event as? SkyBehavior else { return }
switch e {
case .clickSky: print("[Sky] click sky")
case .clickSun: print("[Sky] click sun")
case .clickSunspot(let sunspotId): print("[Sky] click sunspot - id: \(sunspotId)")
}
}
}
優(yōu)化后 userInfo 里的內(nèi)容被關(guān)聯(lián)到枚舉上药薯,Behavior 枚舉中也能方便添加回調(diào)傳值,代碼更加優(yōu)雅直觀救斑,作用域明確童本。代碼SkyProtocolResponderChainPlayground.swift
可以在這個(gè) gist 里找到。
視圖回調(diào)內(nèi)容強(qiáng)制性比之前更強(qiáng)脸候,比如在刪除太陽(yáng)黑子的 id 回調(diào)后穷娱,搞定各處的編譯錯(cuò)誤后,功能也已經(jīng)搞定了运沦。
美中不足
用于處理事件的 UIResponder
(如 SkyViewController)必須在 extension ResponderChainType
的router<Event>(:)
的注釋// Responder handler
與// Other hander ...
之間部分中聲明泵额。
extension ResponderChainType where Self: UIResponder {
public func router<Event>(event: Event) where Event : ResponderChainEventType {
// Responder handler
if let n = next as? SkyViewController {
n.router(event: event)
}
...
}
}
在當(dāng)前版本 Swift 4.0 的派發(fā)機(jī)制下,協(xié)議的 extension 都會(huì)使用直接派發(fā)携添,View 繼承于 UIResponder嫁盲,ResponderChainType 的拓展下,并不能確定 next
的具體類(lèi)型烈掠,只能多寫(xiě)幾行將傳遞的 UIResponder 定位到我們需要的那個(gè)后調(diào)用 router(event:)
方法羞秤。這個(gè)位置的判斷是必要的,但未實(shí)現(xiàn)也是不會(huì)報(bào)錯(cuò)的左敌,容易造成遺漏瘾蛋。
雖然這個(gè)實(shí)現(xiàn)并不是最完美,但相對(duì)直接利用 NSObject 消息派發(fā)的實(shí)現(xiàn)方式母谎,已經(jīng)大大降低了使用時(shí)的不確定性瘦黑。
等到 Swift 在今后更新迭代派發(fā)方式發(fā)生變化,亦或是筆者悟出更完美的解決辦法再來(lái)更新解決這個(gè)不足奇唤。也歡迎大神提點(diǎn)幸斥。
模擬業(yè)務(wù)增刪改
搞了這么多,無(wú)非是想在功能的增刪改時(shí)更加舒適咬扇,在業(yè)務(wù)邏輯變更時(shí)可以少燒些腦細(xì)胞甲葬。怎么驗(yàn)證基于 ResponderChain 的交互真的好用呢?
實(shí)現(xiàn)一個(gè)功能越繁瑣懈贺、代碼分布位置越廣時(shí)经窖,修改時(shí)就越容易遺漏坡垫,我們可以簡(jiǎn)單模擬一下并極度細(xì)化業(yè)務(wù)迭代可能發(fā)生的事,以此分別對(duì)比 Delegate 與 ResponderChain 增刪改所需步驟的繁瑣程度画侣。
增刪:新增太陽(yáng)耀斑視圖 SolarFlareView
冰悠,增加點(diǎn)擊事件
增和刪其實(shí)是一樣的,哪里加的代碼配乱,刪的時(shí)候就得回哪兒刪溉卓,所以這里代碼只舉例增的情況,把增與刪放在一塊分析搬泥。
// ResponderChain
// 1 - 在 Behavior 中添加點(diǎn)擊事件
enum SkyBehavior: ResponderChainEventType {
...
case clickSolarFlare
}
// 2 - 創(chuàng)建視圖
// 3 - 添加點(diǎn)擊事件桑寨,發(fā)送事件
class SolarFlareView: UIView {
...
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
router(event: SkyBehavior.clickSolarFlare)
}
}
// 4 - 處理事件 case
class SkyViewController: UIViewController {
...
func router<Event>(event: Event) where Event : ResponderChainEventType {
guard let e = event as? SkyBehavior else { return }
switch e {
...
case .clickSolarFlare: print("[Sky] click solar flare")
}
}
}
// Delegates
// 1 - 添加視圖 Delegate 聲明
protocol SolarFlareViewDelegate: class {
func solarFlareViewDidTap(_ view: SolarFlareView)
}
// 2 - 創(chuàng)建視圖
// 3 - 視圖內(nèi)聲明 weak var delegate
// 4 - 添加點(diǎn)擊事件,發(fā)送事件
class SolarFlareView: UIView {
weak var delegate: SolarFlareViewDelegate?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
delegate?. solarFlareViewDidTap(self)
}
}
// 5 - 回調(diào)點(diǎn)擊事件到 SunView 中
class SunView: UIView {
let solarFlare = SolarFlareView(frame: .solarFlare)
override init(frame: CGRect) {
super.init(frame: frame)
...
spotView.delegate = self
}
...
}
// 6 - 回調(diào) 太陽(yáng)耀斑視圖
protocol SunViewDelegate: class {
...
func sunView(_ view: SunView, didTapSolarFlare: SolarFlareView)
}
// 7 - 實(shí)現(xiàn) SolarFlareViewDelegate 繼續(xù)傳遞
extension SunView: SolarFlareViewDelegate {
func solarFlareViewDidTap(_ view: SolarFlareView) {
delegate?. sunView(self, didTapSolarFlare: view)
}
}
// 8 - 處理事件
extension SkyViewController: SunViewDelegate {
...
func sunView(_ view: SunView, didTapSolarFlare: SolarFlareView) {
print("[Sky] click solar flare")
}
}
基于 ResponderChain 交互用了 4 步忿檩,而用 Delegates 居然達(dá)到 8 步之多尉尾,其中 Delegates 最繁瑣的流程在太陽(yáng)視圖 SunView 中,相比前者不但步驟多燥透,代碼也分布在各個(gè)文件視圖中沙咏,刪除也需要翻遍各個(gè)文件。
2)改:修改太陽(yáng)耀斑視圖班套,在點(diǎn)擊SolarFlareView
事件中回調(diào) id (注釋老的實(shí)現(xiàn)便于對(duì)比)
// ResponderChain
// 1 - 修改事件芭碍,關(guān)聯(lián)回調(diào)
enum SkyBehavior: ResponderChainEventType {
...
// case clickSolarFlare
case clickSolarFlare(id: Int)
}
// 2 - 修改回調(diào)傳遞
class SolarFlareView: UIView {
...
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// router(event: SkyBehavior.clickSolarFlare)
router(event: SkyBehavior.clickSolarFlare(id: 1))
}
}
// 3 - 修改處理事件 case 獲得 id
class SkyViewController: UIViewController {
...
func router<Event>(event: Event) where Event : ResponderChainEventType {
guard let e = event as? SkyBehavior else { return }
switch e {
...
// case .clickSolarFlare: print("[Sky] click solar flare")
case .clickSolarFlare(let id): print("[Sky] click solar flare, id: \(id)")
}
}
}
// Delegates
// 1 - 修改代理聲明
protocol SolarFlareViewDelegate: class {
// func solarFlareViewDidTap(_ view: SolarFlareView)
func solarFlareView(_ view: SolarFlareView didTap id: Int)
}
// 2 - 修改回調(diào)傳遞
class SolarFlareView: UIView {
weak var delegate: SolarFlareViewDelegate?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// delegate?. solarFlareViewDidTap(self)
delegate?. solarFlareView(self, didTap id: 1)
}
}
// 3 - SunView 中修改代理
protocol SunViewDelegate: class {
...
// func sunView(_ view: SunView, didTapSolarFlare: SolarFlareView)
func sunView(_ view: SunView, didTapSolarFlare: SolarFlareView, solarFlareId: Int)
}
// 4 - SunView 中修改調(diào)用
extension SunView: SolarFlareViewDelegate {
// func solarFlareViewDidTap(_ view: SolarFlareView) {
func solarFlareView(_ view: SolarFlareView didTap id: Int)
// delegate?. sunView(self, didTapSolarFlare: view)
delegate?.sunView(self, didTapSolarFlare: view, solarFlareId: id)
}
}
// 5 - 修改處理事件處
extension SkyViewController: SunViewDelegate {
...
// func sunView(_ view: SunView, didTapSolarFlare: SolarFlareView) {
// print("[Sky] click solar flare")
// }
func sunView(_ view: SunView, didTapSolarFlare: SolarFlareView, solarFlareId: Int) {
print("[Sky] click solar flare, id: \(solarFlareId)")
}
}
基于 ResponderChain 交互用了 3 步,Delegates 用了 5 步孽尽,主要區(qū)別也是在于 SunView 中的修改,多了幾個(gè)傳遞步驟忧勿。
從業(yè)務(wù)的增加上對(duì)比來(lái)說(shuō)杉女,使用基于 ResponderChain 的交互后增刪改迭代功能的步驟都會(huì)變少,單獨(dú)添置內(nèi)容時(shí)也少有影響到別的模塊鸳吸,代碼耦合度降低熏挎,更為清爽。
總結(jié)
視圖多事件回調(diào)處理方案
- Subviews 視圖層級(jí)只有一層晌砾、子視圖回調(diào)方法較少時(shí) 用
Delegates
或Closure
- Subviews 視圖層級(jí)只有一層坎拐、子視圖較多(如3個(gè)以上認(rèn)為已經(jīng)非常繁瑣) 用
ResponderChain
- Subviews 視圖層級(jí)超過(guò)一層 用
ResponderChain
- 不要濫用繼承