Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamically implementing a delegate during runtime

In my class, I have a reference on an UIViewController and want to implement a delegate on this ViewController during runtime. The delegate has only one method (with two parameters) and when the delegate-method on the ViewController is invoked, my class should handle the call.

I am quite sure this is possible with some kind of method swizzling, etc. but I don't know how to accomplish this.

like image 724
danielbuechele Avatar asked Jan 31 '26 03:01

danielbuechele


1 Answers

What you want is possible, but it's not method swizzling, since you don't want to switch to methods but add a new one. It can be done, thanks to Objective-C's dynamic nature, but it's still a dirty hack so also file a feature request with the library vendor.

What you want is class_addMethod() and a C function with the actual implementation for that. One more thing, Objective-C methods are C methods, but with two implicit parameters, self and _cmd, which have to keep in mind (both when creating your C method and when telling class_addMethod your methods signature. And here is an SSCE of how to pull something like that off:

#import <Foundation/Foundation.h>
#import <objc/runtime.h> // Required for class_addMethod()

@interface MyClass : NSObject
@end

@implementation MyClass
@end


@protocol MyProtocol <NSObject>
- (void)printString:(NSString *)string;
@end




// Note the method signature containing the
// two implicit parameters self and _cmd!
void MyClassPrinStringIMP(id self, SEL _cmd, NSString *string)
{
    NSLog(@"Hi I'm %@:%s and this is the string: %@", self, sel_getName(_cmd), string);
}


void PimpMyClass()
{
    // The last argument is the signature. First character is the return type, in our case void
    // Then comes self and _cmd, followed by the NSString. You can use @encode() to find out how your
    // type is encoded. Best is to build this string at runtime, since the encoding can change with architectures
    class_addMethod([MyClass class], @selector(printString:), (IMP)MyClassPrinStringIMP, "v@:@");
}



int main(int argc, const char * argv[])
{
    @autoreleasepool
    {
        PimpMyClass();

        id foo = [[MyClass alloc] init]; // id, to silence the compiler!
        [foo printString:@"Hello World"];
    }

    return 0;
}

Example output:

Hi I'm <MyClass: 0x100101810>:printString: and this is the string: Hello World

Edit: Something that you may find is that the passed object is checked at runtime wether it conforms to a protocol or not using conformsToProtocol:. Since this code just adds the method implementation, it would still fail, but you can tell the runtime that you totally do implement that protocol with this one function call:

class_addProtocol([MyClass class], @protocol(MyProtocol));

Alternative: proxies

Objective-Cs dynamism and message forwarding is already praised by @JasperBlues, however, there is one particular class in Objective-C that is designed to do just that: NSProxy. It is designed to intercept sent messages and dispatching them dynamically to the relevant target, and does use the high-level NSInvocation approach. If you can pass a proxied object in some way as the delegate (depending on what your code allows for and what not), creating a NSProxy subclass might be the cleanest way to go.

However, note though that you then end up with a shim object that wraps over your other object, which comes with its own bag of pain and will break when you try to directly access variables via -> syntax. It's not a perfectly invisible proxy, but good enough for most cases.

like image 64
JustSid Avatar answered Feb 01 '26 17:02

JustSid