2010-08-16 56 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