很慚愧,標題很浮誇,其實就是消息轉發啦。想這個標題想了很久,本想著取個形象生動有意思的標題,不想水平不夠,於是就搞了這麼個奇怪的名字出來。但今天這篇博客的確有那麼點一箭雙雕的意思。
擺事實,講道理。我們先看這麼一個場景。我正在寫一個自定義的 TableView 滾動條,可以實時顯示當前 Cell 的編號,比原生的黑條更具可視性。
實現原理也很簡單,KVO 監聽 contentOffset 然後根據 public func indexPathForRowAtPoint(point: CGPoint) -> NSIndexPath? 返回的 indexPath 顯示編號。contentOffset , contentSize 倒是可以用 KVO,可是否正在滾動、是否正在減速、手指是否離開這些特征盡管有 tracking,dragging,decelerating等屬性可以獲取,但是無法 KVO ,也就是不能實時獲取了。所以只能在回調方法:
optional public func scrollViewWillBeginDragging(scrollView: UIScrollView) optional public func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) optional public func scrollViewDidEndDecelerating(scrollView: UIScrollView)
裡獲取。
為了隱藏細節使暴露的接口盡可能簡單,通常可以把這些代理方法寫在滾動條裡面,也就是讓滾動條成為 tableView 的代理。說到這裡你可能知道問題所在了。如果這時其他類也需要在這些代理方法裡干點什麼(比如有一個刷新控件也需要通過 scrollViewDidEndDragging 監聽手指釋放時機),而這些方法卻被滾送條單獨占有了,顯然就不行了。況且由於 UITableView 的 delegate 中還有 tableView 特有的協議方法(heightForRowAtIndexPath...),一旦 delagte 設置成滾動條了,那麼 ViewController 將無法實現 TableView 的相應功能。
那麼有沒有什麼辦法可以既在滾動條內部監聽這些協議方法,又在 ViewController 裡面監聽這些方法呢?簡而言之就是設置"兩個代理"。
消息轉發。
實現的效果就像下面這樣。
我們只需要使用 NSObject 裡面的這兩個方法就可以實現:
- (void)forwardInvocation:(NSInvocation *)anInvocation OBJC_SWIFT_UNAVAILABLE(""); - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE("");
我們有時會遇到 unrecognized selector sent to instance 的崩潰,原因就是你給一個對象發送了它不具有的消息。而在此之前,運行時其實會去調用一次 - (void)forwardInvocation:(NSInvocation *)anInvocation。顧名思義,這個方法就是用來轉發消息的。你可以實現forwardInvocation:方法來對不能處理的消息做一些默認的處理,以避免程序崩潰。既然可以實現消息的轉發,是不是可以給多個對象發送消息,也就是實現我們上面希望達到的效果 ———— 把一條消息發送給多個對象,即同時激發多個對象的同一個方法。
其實是可以的。我們可以創建一個 delegate 轉發器,作為多個 delegate 的容器,也是對外唯一的 delegate。當消息發送給這個轉發器的時候會觸發對應的 forwardInvocation 方法。我們在這裡進行轉發。
- (void)forwardInvocation:(NSInvocation *)anInvocation{ SEL aSelector = [anInvocation selector]; if([self.firstDelegate respondsToSelector:aSelector]){ [anInvocation invokeWithTarget:self.firstDelegate]; } if([self.secondDelegate respondsToSelector:aSelector]){ [anInvocation invokeWithTarget:self.secondDelegate]; } }
消息可以通過invokeWithTarget:方法發送。
與此同時,我們還需要獲取一下消息的簽名:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ NSMethodSignature *first = [(NSObject *)self.firstDelegate methodSignatureForSelector:aSelector]; NSMethodSignature *second = [(NSObject *)self.secondDelegate methodSignatureForSelector:aSelector]; if(first){ return first; } else if(second) { return second; } return nil; }
另外,非常重要的一步。因為我們這個 delegate 只是作為一個轉發器使用,不是真正用來實現協議方法的,所以還需要重載 respondsToSelector ,讓消息可以轉發到這個中轉站。
- (BOOL)respondsToSelector:(SEL)aSelector{ if([self.firstDelegate respondsToSelector:aSelector] || [self.secondDelegate respondsToSelector:aSelector]){ return YES; } else { return NO; } }
現在 ViewController 裡的 scrollViewDidEndDragging 和滾動條裡的 scrollViewDidEndDragging 代理方法都可以被調用了。至此就實現了一個消息通知多個對象的功能。曲線救國得實現了「一對多」的功能。如果你有更友好的解決方法,歡迎在評論中告訴我。另外還有一個硬傷,就是以上的 workaroud 不支持 Swift 。歡迎討論。