Search code examples
iosobjective-cswizzlingmethod-swizzlingobjc-category

How to create categories dynamically in objective c?


I'm working on an objective-c library project ('MyLib'). Let's say this library is then included in 'TestApp'. Now, 'TestApp' also includes another library called 'Xlib'. This Xlib has a class C1 which has a method m1.

//C1.h is part of Xlib
@interface C1
- (void) m1;
@end

Now, every time m1 is called, MyLib should execute a piece of code. My approach was, if I create a category in MyLib:

@interface C1 (mycat)
@end

@implementation C1 (mycat)
+ (void) load{
    //code to swizzle method in class C1 from: @selector(m1) to: @selector(mycat_m1)
}
- (void) mycat_m1{
    /*
      insert code that I want to execute
    */
     [self mycat_m1]; //calling the original m1 implementation
}
@end

Problem: MyLib doesn't have the class C1. So I can't build MyLib as I'm trying to create a category on a class which doesn't exist. Hence, compilation errors.

So, then I tried to wrap the above code inside:

#if defined(__has_include)
#if __has_include("C1.h")
    /* above category code */
#endif
#endif

MyLib compiles and builds fine now, but since C1.h is not present in MyLib, the mylib.framework wouldn't have this category.

Now, I have two options: 1. Create this category dynamically, so that once the library is included in the app, and then when the app runs, this category will be created depending on whether TestApp includes Xlib or not. 2. Remove the file which has this category code from compile sources, and then somehow expose that file in my framework to TestApp.

I'm not able to solve either of the options. Any ideas on the existing options? or any new options?

Edit: Added details since the question wasn't quite explanatory


Solution

  • This code doesn't need to be in a category.

    Categories are often used when swizzling because most of the time, people are trying to "wrap" a function with their own behavior in a specific class. Swizzling is often done in the +load class method. The load class method on a Category is a nice place to put it organizationally, because you know the main class's +load method has just been called.

    Swizzling is still quite possible in your case. Implement a class in your framework with a +load method. The swizzling code will go there like usual. You just need to lookup the Class you want to swizzle, instead of assuming the target reference is to self like it would be if this was in a Category. That is, if you are referencing a blog post or using a favorite technique to swizzle that references self, know that it likely won't be. Make sure you watch out for what class you are trying to swizzle to/from.

    I've had to do something similar in the past. In my case, I had to look up the instance of the class that implemented the AppDelegate protocol from a Framework. In that case, the code that triggered the swizzling wasn't called from +load (since the AppDelegate class might not have been loaded, and the instance of the class for the AppDelgate definitely wasn't instantiated). In that case, I invoked the code from my class's -init method, and protected it so it couldn't be called twice. However, I was guaranteed to be able to instantiate my class from the app code, so the technique worked; I'm not 100% sure of your use case. It wouldn't hurt to post the code you've tried.

    UPDATE: concrete example

    Here is the method I keep handy for swizzling.

    + (void)swizzleSelector:(SEL)sel1 onClass:(Class)class1 withSelector:(SEL)sel2 fromClass:(Class)class2
    {
        Method method1 = class_getInstanceMethod(class1, sel1);
        Method method2 = class_getInstanceMethod(class2, sel2);
    
        // Make sure that both methods are on the target class (the one to swizzle likely already is).
        class_addMethod(class1,
                        sel1,
                        method_getImplementation(method1),
                        method_getTypeEncoding(method1));
        class_addMethod(class1, // The swizzling is 'on' the first class, so it's the target here, not class2.
                        sel2,
                        method_getImplementation(method2),
                        method_getTypeEncoding(method2));
    
        // Once they are both added to the class, exchange the implementations of the methods.
        method_exchangeImplementations(class_getInstanceMethod(class1,sel1),
                                       class_getInstanceMethod(class1,sel2));
    }
    

    Like I said, you'll need to lookup the class of your target somehow, but assuming you are calling this from the class that has your replacement methods, and assuming m1: is the target selector you are trying to swizzle, you might invoke like this:

    Class class = NSClassFromString(@"C1");
    // Check that `class` is not `nil`
    
    [self swizzleSelector:@selector(m1)
                  onClass:class
             withSelector:@selector(my_m1)
                fromClass:self];
    

    I hope this helps to clarify what you can do with swizzling. 999 out of 1000, you probably don't need to swizzle. Your use case sounds like a possibility where you might have to.