2010-08-16 66 views
31

我有一個UIScrollView的子類,我需要在內部響應滾動行爲。但是,viewcontroller仍然需要監聽滾動委託回調,所以我不能徹底竊取組件中的委託。攔截Objective-C在子類中委託消息

有沒有辦法讓名爲「委託」的屬性只聽信息發送,否則以某種方式內部劫持委託屬性和運行一些代碼後向外轉發消息?

回答

-5

是的,但是您必須重寫the docs中的每個委託方法。基本上,製作第二個委託屬性並實施委託協議。當你的委託方法被調用時,照顧你的業務,然後在你剛剛運行的委託方法的第二個委託中調用相同的方法。例如。

- (void)scrollViewDidScroll:(UIScrollView *)scrollView { 
    // Do stuff here 
    if ([self.delegate2 respondsToSelector:@selector(scrollViewDidScroll:)]) { 
     [self.delegate2 scrollViewDidScroll:scrollView]; 
    } 
} 
+3

Uglyyyyyyyyy,但不幸的是必要的:/ – 2010-08-17 03:03:40

+0

呀,唯一的其他選擇是使用nsnotifications – 2010-08-17 05:30:56

+0

你好......我實施了對我自己的代碼,對UITextField一些委託方法它的工作原理,但與其他導致無法識別的選擇器發送到實例0x4b21800' – speeder 2011-03-03 15:25:46

85

爲避免手動覆蓋所有委託方法,可以使用消息轉發。我只是實現通過中間代理類同樣的事情如下:

MessageInterceptor.h

@interface MessageInterceptor : NSObject { 
    id receiver; 
    id middleMan; 
} 
@property (nonatomic, assign) id receiver; 
@property (nonatomic, assign) id middleMan; 
@end 

MessageInterceptor.m

@implementation MessageInterceptor 
@synthesize receiver; 
@synthesize middleMan; 

- (id)forwardingTargetForSelector:(SEL)aSelector { 
    if ([middleMan respondsToSelector:aSelector]) { return middleMan; } 
    if ([receiver respondsToSelector:aSelector]) { return receiver; } 
    return [super forwardingTargetForSelector:aSelector]; 
} 

- (BOOL)respondsToSelector:(SEL)aSelector { 
    if ([middleMan respondsToSelector:aSelector]) { return YES; } 
    if ([receiver respondsToSelector:aSelector]) { return YES; } 
    return [super respondsToSelector:aSelector]; 
} 

@end 

MyScrollView.h

#import "MessageInterceptor.h" 

@interface MyScrollView : UIScrollView { 
    MessageInterceptor * delegate_interceptor; 
    //... 
} 

//... 

@end 

MyScrollView.m(編輯,以感謝jhabbott):

@implementation MyScrollView 

- (id)delegate { return delegate_interceptor.receiver; } 

- (void)setDelegate:(id)newDelegate { 
    [super setDelegate:nil]; 
    [delegate_interceptor setReceiver:newDelegate]; 
    [super setDelegate:(id)delegate_interceptor]; 
} 

- (id)init* { 
    //... 
    delegate_interceptor = [[MessageInterceptor alloc] init]; 
    [delegate_interceptor setMiddleMan:self]; 
    [super setDelegate:(id)delegate_interceptor]; 
    //... 
} 

- (void)dealloc { 
    //... 
    [delegate_interceptor release]; 
    //... 
} 

// delegate method override: 
- (void)scrollViewDidScroll:(UIScrollView *)scrollView { 
    // 1. your custom code goes here 
    // 2. forward to the delegate as usual 
    if ([self.delegate respondsToSelector:@selector(scrollViewDidScroll:)]) { 
     [self.delegate scrollViewDidScroll:scrollView]; 
    } 
} 

@end 

有了這種方法,MessageInterceptor對象會自動將所有會議代表的消息到正規的委託對象,除了的那些你在你的自定義子類中重寫。

+5

這是一個非常優雅的解決方案,現在我已經使用了它幾次。非常好。 – 2011-05-16 10:54:44

+0

@Simon Lee:我很高興它有用。感謝您花時間讓我知道':))' – 2011-05-16 18:39:18

+7

這對我來說非常有用。其他人在UITextView中使用這兩點。第一個UITextView顯然在內部是指self.delegate,所以它跳過了你的中間人。修正是刪除jhabbot的「(id)委託...」getter行(這意味着你的中間人的委託方法必須直接調用「真正的」委託)。其次,它會檢查委託在調用它們之前是否實現了某些內部例程,這會導致無限遞歸。修正是添加「if([[middleMan超類] instancesRespondToSelector:aSelector])返回NO;」在respondsToSelector頂部: – mackworth 2012-05-09 17:36:19

64

來自e.James的帖子爲大多數意見提供了一個很好的解決方案。但對於像UITextField和UITextView這樣的鍵盤依賴視圖,它通常會導致無限循環。爲了擺脫它,我用一些額外的代碼修復了它,檢查選擇器是否包含在特定的協議中。

WZProtocolInterceptor.h

#import <Foundation/Foundation.h> 

@interface WZProtocolInterceptor : NSObject 
@property (nonatomic, readonly, copy) NSArray * interceptedProtocols; 
@property (nonatomic, weak) id receiver; 
@property (nonatomic, weak) id middleMan; 

- (instancetype)initWithInterceptedProtocol:(Protocol *)interceptedProtocol; 
- (instancetype)initWithInterceptedProtocols:(Protocol *)firstInterceptedProtocol, ... NS_REQUIRES_NIL_TERMINATION; 
- (instancetype)initWithArrayOfInterceptedProtocols:(NSArray *)arrayOfInterceptedProtocols; 
@end 

WZProtocolInterceptor.m

#import <objc/runtime.h> 

#import "WZProtocolInterceptor.h" 

static inline BOOL selector_belongsToProtocol(SEL selector, Protocol * protocol); 

@implementation WZProtocolInterceptor 
- (id)forwardingTargetForSelector:(SEL)aSelector 
{ 
    if ([self.middleMan respondsToSelector:aSelector] && 
     [self isSelectorContainedInInterceptedProtocols:aSelector]) 
     return self.middleMan; 

    if ([self.receiver respondsToSelector:aSelector]) 
     return self.receiver; 

    return [super forwardingTargetForSelector:aSelector]; 
} 

- (BOOL)respondsToSelector:(SEL)aSelector 
{ 
    if ([self.middleMan respondsToSelector:aSelector] && 
     [self isSelectorContainedInInterceptedProtocols:aSelector]) 
     return YES; 

    if ([self.receiver respondsToSelector:aSelector]) 
     return YES; 

    return [super respondsToSelector:aSelector]; 
} 

- (instancetype)initWithInterceptedProtocol:(Protocol *)interceptedProtocol 
{ 
    self = [super init]; 
    if (self) { 
     _interceptedProtocols = @[interceptedProtocol]; 
    } 
    return self; 
} 

- (instancetype)initWithInterceptedProtocols:(Protocol *)firstInterceptedProtocol, ...; 
{ 
    self = [super init]; 
    if (self) { 
     NSMutableArray * mutableProtocols = [NSMutableArray array]; 
     Protocol * eachInterceptedProtocol; 
     va_list argumentList; 
     if (firstInterceptedProtocol) 
     { 
      [mutableProtocols addObject:firstInterceptedProtocol]; 
      va_start(argumentList, firstInterceptedProtocol); 
      while ((eachInterceptedProtocol = va_arg(argumentList, id))) { 
       [mutableProtocols addObject:eachInterceptedProtocol]; 
      } 
      va_end(argumentList); 
     } 
     _interceptedProtocols = [mutableProtocols copy]; 
    } 
    return self; 
} 

- (instancetype)initWithArrayOfInterceptedProtocols:(NSArray *)arrayOfInterceptedProtocols 
{ 
    self = [super init]; 
    if (self) { 
     _interceptedProtocols = [arrayOfInterceptedProtocols copy]; 
    } 
    return self; 
} 

- (void)dealloc 
{ 
    _interceptedProtocols = nil; 
} 

- (BOOL)isSelectorContainedInInterceptedProtocols:(SEL)aSelector 
{ 
    __block BOOL isSelectorContainedInInterceptedProtocols = NO; 
    [self.interceptedProtocols enumerateObjectsUsingBlock:^(Protocol * protocol, NSUInteger idx, BOOL *stop) { 
     isSelectorContainedInInterceptedProtocols = selector_belongsToProtocol(aSelector, protocol); 
     * stop = isSelectorContainedInInterceptedProtocols; 
    }]; 
    return isSelectorContainedInInterceptedProtocols; 
} 

@end 

BOOL selector_belongsToProtocol(SEL selector, Protocol * protocol) 
{ 
    // Reference: https://gist.github.com/numist/3838169 
    for (int optionbits = 0; optionbits < (1 << 2); optionbits++) { 
     BOOL required = optionbits & 1; 
     BOOL instance = !(optionbits & (1 << 1)); 

     struct objc_method_description hasMethod = protocol_getMethodDescription(protocol, selector, required, instance); 
     if (hasMethod.name || hasMethod.types) { 
      return YES; 
     } 
    } 

    return NO; 
} 

這裏是斯威夫特2版:

// 
// NSProtocolInterpreter.swift 
// Nest 
// 
// Created by Manfred Lau on 11/28/14. 
// Copyright (c) 2014 WeZZard. All rights reserved. 
// 

import Foundation 

/** 
`NSProtocolInterceptor` is a proxy which intercepts messages to the middle man 
which originally intended to send to the receiver. 

- Discussion: `NSProtocolInterceptor` is a class cluster which dynamically 
subclasses itself to conform to the intercepted protocols at the runtime. 
*/ 
public final class NSProtocolInterceptor: NSObject { 
    /// Returns the intercepted protocols 
    public var interceptedProtocols: [Protocol] { return _interceptedProtocols } 
    private var _interceptedProtocols: [Protocol] = [] 

    /// The receiver receives messages 
    public weak var receiver: NSObjectProtocol? 

    /// The middle man intercepts messages 
    public weak var middleMan: NSObjectProtocol? 

    private func doesSelectorBelongToAnyInterceptedProtocol(
     aSelector: Selector) -> Bool 
    { 
     for aProtocol in _interceptedProtocols 
      where sel_belongsToProtocol(aSelector, aProtocol) 
     { 
      return true 
     } 
     return false 
    } 

    /// Returns the object to which unrecognized messages should first be 
    /// directed. 
    public override func forwardingTargetForSelector(aSelector: Selector) 
     -> AnyObject? 
    { 
     if middleMan?.respondsToSelector(aSelector) == true && 
      doesSelectorBelongToAnyInterceptedProtocol(aSelector) 
     { 
      return middleMan 
     } 

     if receiver?.respondsToSelector(aSelector) == true { 
      return receiver 
     } 

     return super.forwardingTargetForSelector(aSelector) 
    } 

    /// Returns a Boolean value that indicates whether the receiver implements 
    /// or inherits a method that can respond to a specified message. 
    public override func respondsToSelector(aSelector: Selector) -> Bool { 
     if middleMan?.respondsToSelector(aSelector) == true && 
      doesSelectorBelongToAnyInterceptedProtocol(aSelector) 
     { 
      return true 
     } 

     if receiver?.respondsToSelector(aSelector) == true { 
      return true 
     } 

     return super.respondsToSelector(aSelector) 
    } 

    /** 
    Create a protocol interceptor which intercepts a single Objecitve-C 
    protocol. 

    - Parameter  protocols: An Objective-C protocol, such as 
    UITableViewDelegate.self. 
    */ 
    public class func forProtocol(aProtocol: Protocol) 
     -> NSProtocolInterceptor 
    { 
     return forProtocols([aProtocol]) 
    } 

    /** 
    Create a protocol interceptor which intercepts a variable-length sort of 
    Objecitve-C protocols. 

    - Parameter  protocols: A variable length sort of Objective-C protocol, 
    such as UITableViewDelegate.self. 
    */ 
    public class func forProtocols(protocols: Protocol ...) 
     -> NSProtocolInterceptor 
    { 
     return forProtocols(protocols) 
    } 

    /** 
    Create a protocol interceptor which intercepts an array of Objecitve-C 
    protocols. 

    - Parameter  protocols: An array of Objective-C protocols, such as 
    [UITableViewDelegate.self]. 
    */ 
    public class func forProtocols(protocols: [Protocol]) 
     -> NSProtocolInterceptor 
    { 
     let protocolNames = protocols.map { NSStringFromProtocol($0) } 
     let sortedProtocolNames = protocolNames.sort() 
     let concatenatedName = sortedProtocolNames.joinWithSeparator(",") 

     let theConcreteClass = concreteClassWithProtocols(protocols, 
      concatenatedName: concatenatedName, 
      salt: nil) 

     let protocolInterceptor = theConcreteClass.init() 
      as! NSProtocolInterceptor 
     protocolInterceptor._interceptedProtocols = protocols 

     return protocolInterceptor 
    } 

    /** 
    Return a subclass of `NSProtocolInterceptor` which conforms to specified 
     protocols. 

    - Parameter  protocols:   An array of Objective-C protocols. The 
    subclass returned from this function will conform to these protocols. 

    - Parameter  concatenatedName: A string which came from concatenating 
    names of `protocols`. 

    - Parameter  salt:    A UInt number appended to the class name 
    which used for distinguishing the class name itself from the duplicated. 

    - Discussion: The return value type of this function can only be 
    `NSObject.Type`, because if you return with `NSProtocolInterceptor.Type`, 
    you can only init the returned class to be a `NSProtocolInterceptor` but not 
    its subclass. 
    */ 
    private class func concreteClassWithProtocols(protocols: [Protocol], 
     concatenatedName: String, 
     salt: UInt?) 
     -> NSObject.Type 
    { 
     let className: String = { 
      let basicClassName = "_" + 
       NSStringFromClass(NSProtocolInterceptor.self) + 
       "_" + concatenatedName 

      if let salt = salt { return basicClassName + "_\(salt)" } 
       else { return basicClassName } 
     }() 

     let nextSalt = salt.map {$0 + 1} 

     if let theClass = NSClassFromString(className) { 
      switch theClass { 
      case let anInterceptorClass as NSProtocolInterceptor.Type: 
       let isClassConformsToAllProtocols: Bool = { 
        // Check if the found class conforms to the protocols 
        for eachProtocol in protocols 
         where !class_conformsToProtocol(anInterceptorClass, 
          eachProtocol) 
        { 
         return false 
        } 
        return true 
        }() 

       if isClassConformsToAllProtocols { 
        return anInterceptorClass 
       } else { 
        return concreteClassWithProtocols(protocols, 
         concatenatedName: concatenatedName, 
         salt: nextSalt) 
       } 
      default: 
       return concreteClassWithProtocols(protocols, 
        concatenatedName: concatenatedName, 
        salt: nextSalt) 
      } 
     } else { 
      let subclass = objc_allocateClassPair(NSProtocolInterceptor.self, 
       className, 
       0) 
       as! NSObject.Type 

      for eachProtocol in protocols { 
       class_addProtocol(subclass, eachProtocol) 
      } 

      objc_registerClassPair(subclass) 

      return subclass 
     } 
    } 
} 

/** 
Returns true when the given selector belongs to the given protocol. 
*/ 
public func sel_belongsToProtocol(aSelector: Selector, 
    _ aProtocol: Protocol) -> Bool 
{ 
    for optionBits: UInt in 0..<(1 << 2) { 
     let isRequired = optionBits & 1 != 0 
     let isInstance = !(optionBits & (1 << 1) != 0) 

     let methodDescription = protocol_getMethodDescription(aProtocol, 
      aSelector, isRequired, isInstance) 

     if !objc_method_description_isEmpty(methodDescription) 
     { 
      return true 
     } 
    } 
    return false 
} 

public func objc_method_description_isEmpty(
    var methodDescription: objc_method_description) 
    -> Bool 
{ 
    let ptr = withUnsafePointer(&methodDescription) { UnsafePointer<Int8>($0) } 
    for offset in 0..<sizeof(objc_method_description) { 
     if ptr[offset] != 0 { 
      return false 
     } 
    } 
    return true 
} 
+2

我真的很喜歡這個代碼!我將它用於'UITextField',它效果很好。 – Erik 2014-02-03 16:57:02

+3

不得不獎勵這個答案+100賞金是迄今爲止最好的解決方案。它只是需要更多的關注,因爲它遲到了。您獲得的upvotes大量涌入是從[meta](http://meta.stackoverflow.com/a/291533/2792531)鏈接到此問題的結果。 – nhgrif 2015-04-28 10:48:34

+0

感謝您的讚賞。這是我分享知識的動力。 – WeZZard 2015-04-29 05:30:56

4

其實,這爲我工作:

@implementation MySubclass { 
    id _actualDelegate; 
} 

// There is no need to set the value of _actualDelegate in an init* method 
- (void)setDelegate:(id)newDelegate { 
    [super setDelegate:nil]; 
    _actualDelegate = newDelegate; 
    [super setDelegate:(id)self]; 
} 

- (id)delegate { 
    return self; 
} 

- (id)forwardingTargetForSelector:(SEL)aSelector { 
    if ([_actualDelegate respondsToSelector:aSelector]) { return _actualDelegate; } 
    return [super forwardingTargetForSelector:aSelector]; 
} 

- (BOOL)respondsToSelector:(SEL)aSelector { 
    return [super respondsToSelector:aSelector] || [_actualDelegate respondsToSelector:aSelector]; 
} 
@end 

...使子類成爲e.James給出的真棒回答中的消息攔截器。

+0

我喜歡這個答案,因爲它不需要另一個類。不知道爲什麼委託人在設置爲自己之前設置爲零。另外,應該將actualDelegate定義爲assign屬性,以防止它被保留。 – malhal 2016-01-04 13:42:21