【问题标题】:Intercept Objective-C delegate messages within a subclass在子类中拦截 Objective-C 委托消息
【发布时间】:2011-03-30 17:10:14
【问题描述】:

我有一个 UIScrollView 的子类,我需要在其中内部响应滚动行为。但是,视图控制器仍然需要监听滚动委托回调,所以我不能直接在我的组件中窃取委托。

有没有办法保留名为“delegate”的属性并只收听沿它发送的消息,或者以某种方式在内部劫持委托属性并在运行一些代码后向外转发消息?

【问题讨论】:

    标签: ios objective-c objective-c-runtime


    【解决方案1】:

    为避免手动覆盖所有委托方法,您可以使用消息转发。我刚刚使用中间代理类实现了同样的事情,如下所示:

    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 对象将自动将所有委托消息转发到常规委托对象,除了您在自定义子类中覆盖的那些。

    【讨论】:

    • 这是一个非常优雅的解决方案,我已经用过几次了。非常好。
    • @Simon Lee:我很高兴它有用。感谢您抽出宝贵时间告诉我:)
    • 这对我很有用。使用 UITextView 的其他人有两点。第一个 UITextView 显然在内部引用了 self.delegate,所以它跳过了你的中间人。修复是删除 jhabbot 的“(id) delegate...”getter 行(这意味着您的中间人的委托方法必须直接调用“真实”委托)。其次,它在调用之前检查委托是否实现了某些内部例程,这会导致无限递归。修复是添加“if ([[middleMan superclass] instancesRespondToSelector:aSelector]) return NO;”在响应选择器顶部:
    • @mackworth:酷。感谢您发布此信息。我自己没有用UITextView 尝试过,所以我很高兴你能够让它工作
    • iOS8 上为我工作,但为我检查[self.delegate respondsToSelector:] 必须更改为[delegate_interceptor.receiver respondsToSelector:]。此外,因此类在某些选择器上存在问题,例如 UITextField 子类卡在 keyboardInputChanged: 上,这是我从未听说过的......
    【解决方案2】:

    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;
    }
    

    这里是 Swift 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
    }
    

    【讨论】:

    • 我真的很喜欢这个代码!我将它用于UITextField,效果很好。
    • 不得不奖励这个答案 +100 赏金,因为它是迄今为止最好的解决方案。它只是需要更多的关注,因为它发布得很晚。你得到的大量赞成票是这个问题与meta相关联的结果。
    • 感谢您的赞赏。这是我分享知识的动力。
    • 我已将其添加到CocoaPods。随意收回它。
    • Swift 2 版本在尝试用于 UITextField 时在 if 语句的 sel_belongsToProtocol 中崩溃,并带有 EXC_BAD_ACCESS
    【解决方案3】:

    实际上,这对我有用:

    @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 给出的绝妙答案中的消息拦截器。

    【讨论】:

    • 我更喜欢这个答案,因为它不需要其他课程。不确定为什么在设置为 self 之前将委托设置为 nil 。此外,actualDelegate 应定义为分配属性,以防止其保留。
    【解决方案4】:

    是的,但是您必须覆盖the docs 中的每个委托方法。基本上,创建第二个委托属性并实现委托协议。当您的委托方法被调用时,请处理好您的业务,然后从刚刚运行的委托方法中对您的第二个委托调用相同的方法。例如。

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

    【讨论】:

    • Uglyyyyyyyyy,但不幸的是必要的:/
    • 是的,唯一的其他选择是使用 nsnotifications
    • 您好...我在我自己的代码中实现了这一点,对于带有一些委托方法的 UITextField 它可以工作,但对于其他方法会导致无法识别的选择器发送到实例 0x4b21800'
    • 您必须在要向其发送消息的实例上实现该方法。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-03-05
    • 2010-09-10
    • 2015-11-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-05-14
    相关资源
    最近更新 更多