使用 Swift 時,如果是自定義的 Protocol,可以通過 Extension 來提供部分方法的預設實現,但系統原有的 Protocol 卻不行,大概是因為系統的 Protocol 是 Objective-C 實現的緣故。
但為 Protocol 提供預設實現在某些時候是很實用的。比如 iOS 開發中,Cocoa 框架裡常用的 TableView 使用時一般需要實現 UITableViewDataSouce 和 UITableViewDelegate。而 UITableViewDelegate 和 UITableViewDataSource 裡面的一部分方法,又是可以抽離的,但剩下的一部分方法,使用得很少,且一般用起來都是有自定義的需要。那麼有沒有一種方法可以為一部分方法提供預設通用的實現,而又不影響其餘的方法呢?
這篇文章,筆者想分享一種通過訊息轉發的方式來實現差不多功能的方法。
Objective-C 的訊息轉發
本文假設讀者已經瞭解 Runtime 基礎,如不瞭解可自行閱讀 Objective-C Runtime 1小時入門教程。
引用網上這張經典的圖,可以知道在訊息處理無法處理之前,有三個環節能進行搶救。第一步判斷是否有動態新增方法,第二步可以對訊息處理者重定向,第三步可以在返回方法簽名的情況下,對封裝訊息的相關資訊的 NSInvocation 進行處理。
實現 ProtocolProxy
通過上面的訊息轉發的步驟,筆者的思路是,利用重定向,將 Protocol 裡所有的實現進行轉發,Protocol 如果有自定義實現者則交給它,否則交給預設實現者,如果都沒有則不處理。
用 Swift 簡單的實現如下:
class ProtocolProxy: NSObject {
weak var customImp: AnyObject?
weak var defaultImp: AnyObject?
override func responds(to aSelector: Selector!) -> Bool {
return defaultImp?.responds(to: aSelector) == true || customImp?.responds(to:aSelector) == true
}
override func forwardingTarget(for aSelector: Selector!) -> Any? {
if let rawObject = customImp, rawObject.responds(to: aSelector) {
return rawObject
} else if let defualtObject = defaultImp, defualtObject.responds(to: aSelector) {
return defaultImp
} else {
return super.forwardingTarget(for: aSelector)
}
}
}
複製程式碼
其中 respondsToSelector 方法用於判斷方法是否有被實現者實現,有的話通過 forwardingTarget 方法進行訊息處理者的重定向。
TableView 的實際使用
實現了 ProtocolProxy 之後,在設定 Protocol 時,就可以設定成 ProtocolProxy,然後將預設實現和自定義實現都設定給 ProtocolProxy。舉個 TableView 的例子:
private let delegateKey = "delegate"
private let dataSourceKey = "dataSource"
class ProxyTableView: UITableView {
let delegateProxy = ProtocolProxy()
let dataSourceProxy = ProtocolProxy()
var defaultDelgate: UITableViewDelegate? {
didSet{
delegateProxy.defaultImp = defaultDelgate
setValue(delegateProxy, forKey: delegateKey)
}
}
var defaultDataSource: UITableViewDataSource? {
didSet{
dataSourceProxy.defaultImp = defaultDataSource
setValue(dataSourceProxy, forKey: dataSourceKey)
}
}
override public var dataSource: UITableViewDataSource?{
didSet{
if !(dataSource is ProtocolProxy){
dataSourceProxy.customImp = dataSource
setValue(dataSourceProxy, forKey: dataSourceKey)
}
}
}
override public var delegate: UITableViewDelegate?{
didSet{
if !(delegate is ProtocolProxy){
delegateProxy.customImp = delegate
setValue(delegateProxy, forKey: delegateKey)
}
}
}
}
複製程式碼
ProxyTableView 持有 delegate 和 dataSource 的 ProtocolProxy,通過重寫 delegate 和 dataSource 的 didSet 屬性觀察方法,將 Protocol 的自定義實現設定給對應 ProtocolProxy,再將 ProtocolProxy 設定為 TableView 的 Delegate 和 DataSource,這裡需要注意的是,要使用 KVC 進行設定,因為 ProtocolProxy 本身並不實現 UITableViewDelegate 和 UITableViewDataSource,而且需要判斷是不是 ProtocolProxy 避免迴圈設定。再加上 defaultDataSouce 和 defaultDelegate 來讓外界設定預設實現者,設定完需要重新用 KVC 設定到 TableView 裡。
這樣的話,就可以將一部分預設實現抽離到基類中,而對於使用者來說,僅需要實現部分自定義的方法。
比如想讓 didSelectRowAtIndexPath
的實現自定義化,可以像例子中這樣
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let tableView = ProxyTableView(frame: view.frame, style: .plain)
//...
tableView.defaultDelgate = BaseTableViewDelegate()
tableView.delegate = self
}
}
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("CustomTableViewDelegate")
}
}
class BaseTableViewDelegate: NSObject, UITableViewDelegate{
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("BaseTableViewDelegate")
}
}
複製程式碼
最後
完整的 Demo,別的系統 Protocol 也可以用同樣的方式進行擴充套件,提供一部分方法的預設實現。