給 Cocoa 的系統 Protocol 提供預設實現

Nemocdz發表於2019-02-07

使用 Swift 時,如果是自定義的 Protocol,可以通過 Extension 來提供部分方法的預設實現,但系統原有的 Protocol 卻不行,大概是因為系統的 Protocol 是 Objective-C 實現的緣故。

但為 Protocol 提供預設實現在某些時候是很實用的。比如 iOS 開發中,Cocoa 框架裡常用的 TableView 使用時一般需要實現 UITableViewDataSouce 和 UITableViewDelegate。而 UITableViewDelegate 和 UITableViewDataSource 裡面的一部分方法,又是可以抽離的,但剩下的一部分方法,使用得很少,且一般用起來都是有自定義的需要。那麼有沒有一種方法可以為一部分方法提供預設通用的實現,而又不影響其餘的方法呢?

這篇文章,筆者想分享一種通過訊息轉發的方式來實現差不多功能的方法。

Objective-C 的訊息轉發

本文假設讀者已經瞭解 Runtime 基礎,如不瞭解可自行閱讀 Objective-C Runtime 1小時入門教程

691078-9049faadbcbbeac9

引用網上這張經典的圖,可以知道在訊息處理無法處理之前,有三個環節能進行搶救。第一步判斷是否有動態新增方法,第二步可以對訊息處理者重定向,第三步可以在返回方法簽名的情況下,對封裝訊息的相關資訊的 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 也可以用同樣的方式進行擴充套件,提供一部分方法的預設實現。

相關文章