Search code examples
iosobjective-csubclassobjective-c-category

How to point allocWithZone: to a subclass under ARC (NOT a singleton!)


I've seen a few SO questions similar to this one, but they all involve singletons, and the answers are all (correctly) "don't do that, use dispatch_once() instead."

In my particular instance, I'm not making a singleton, and I need for the superclass's allocWithZone: to return an instance of my subclass.

What I'm trying to do is: I want to make an extension to an existing library that I don't own. Rather than doing a bunch of behaviours in categories -- which won't even work properly if I'm overriding existing behaviour, because which-of-many same-signature methods gets called isn't defined -- I want it to be that, when someone includes my (very small) category, then calls

LibObject *myObj = [[LibObject alloc] init];

They get back, instead, as if they'd called

LibObject *myObj = [[MyLibObjSubclass alloc] init];

where LibrarySubclass is the subclass which I'm creating. In particular, All of the library's objects that are subclasses of LibObject, I want them, instead, to be subclasses of MyLibObjSubclass.

I've messed with swizzling and categories and everything else I can think of, but I always end up with one of 2 problems: either

  • I end up crashing after an infinite recursion of [MyLibObjSubclass alloc] calling [LibObject alloc] calling [MyLibObjSubclass alloc], etc., or
  • Everything that is a subclass of LibObject, rather than being a subclass of MyLibObjSubclass, with its new behaviours, it just is a MyLibObjSubclass, without any of the additional behaviours.

For this 2nd problem, by way of example, let's say I was trying to override all UIView-s to be MyView-s, where MyView is a subclass of UIView. The problem is that, when I try to category-in (as part of UIView+MyShim), then this line:

UILabel *label = [[UILabel alloc] init];

instead of returning a UILabel that is a subclass of MyView -> UIView -> UIResponder -> NSObject, it just returns a MyView which, for example, doesn't contain a text property, so all UILabel-s are broken.

So my question is: how do I do what I want?

Thanks!


Solution

  • It sounds like you want to modify the class hierarchy from this:

    CCResponder
     └─ CCNode
         ├─ CCControl
         └─ CCSprite
    

    to this:

    CCResponder
     └─ MyResponder
         └─ CCNode
             ├─ CCControl
             └─ CCSprite
    

    Since class_setSuperclass is deprecated, there's no supported way to make that change at runtime.

    (NOTE: Yes, that's exactly what I was hoping to do. ~Olie.)

    Have you considered just modifying the Cocos2D source code? Short-term, this should be the simplest approach. If what you need is general enough, maybe you can get your changes merged upstream. Or maybe you can add hooks that will be accepted upstream, and use those hooks to do what you need.

    If you're keeping your code in git, then I recommend using the git-subtree extension to import the Cocos2D source code into your project's repository. This makes it pretty easy to keep track of your local changes to Cocos2D and reapply them when you want to import a newer version.

    If that doesn't work for you, I think you should just suck it up and use swizzling on the methods whose behaviors you want to change.

    If you are a glutton for punishment, you can create a new subclass of each CCResponder subclass, on demand at runtime. You'll end up with a hierarchy like this:

    CCResponder
     └─ CCNode
         ├─ CCControl
         │   └─ hacked_CCControl
         └─ CCSprite
             └─ hacked_CCSprite
    

    (I'm aware that CCControl isn't something you're likely to instantiate directly but let's pretend.)

    I think this is possible, but pretty complicated. To do it, swizzle +[CCResponder alloc] with (your own) +[CCResponder allocHack]. You would implement something like this:

    static Class createHackedClassForClass(Class originalClass) {
        NSString *name = [@"hacked_" stringByAppendingString:NSStringFromClass(originalClass)];
        Class hackedClass = objc_allocateClassPair(originalClass, name.UTF8String, 0);
    
        // Here add methods as necessary to hackedClass using class_addMethod.
    
        objc_registerClassPair(hackedClass);
        return hackedClass;
    }
    
    + (void)allocHack {
        static NSMutableDictionary *hackedClassForClass = [NSMutableDictionary dictionary];
        Class hackedClass = hackedClassForClass[self];
        if (hackedClass == nil) {
            hackedClass = createHackedClassForClass(self);
            hackedClassForClass[self] = hackedClass;
        }
        return class_createInstance(hackedClass, 0);
    }
    

    I haven't tried this so I don't know if there's any major problems with it. It will at a minimum make things harder to debug than just modifying the Cocos2D source code.