1

I have a custom container view controller that manages the view hierarchy of my app. I know that every controller is some sort of child of this container controller. I thought it would be nice to have a category on UIViewController that would allow me to access the container controller, no matter where I am in the hierarchy.

This involves a recursive walk up the controller hierarchy, so I thought it would be good to try and only do that walk once per controller. So with objc_setAssociatedObject, I set the container once I've found it and set a flag so that I know whether or not I need to walk the hierarchy on subsequent calls (I planned to invalidate that if the viewcontroller ever moved, but that's probably overkill, and I didn't get that far).

Anyway, that works fine for the most part except that my flag for whether or not the hierarchy has been walked seems to be attached to UIViewController, and not specific subclasses of UIViewController.

I swizzled +load to try to set default values on my associated objects to no avail.

Any ideas? How to I get associated objects in a category to associate with the subclasses of the class the category is defined on?

Here's my code, for good measure.

#import "UIViewController+LMPullMenuContainer.h"
#import <objc/runtime.h>

static char const * const CachedKey = "__LM__CachedBoolPullMenuAssociatedObjectKey";
static char const * const PullMenuKey = "__LM__PullMenuAssociatedObjectKey";

@implementation UIViewController (LMPullMenuContainer)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL initSelector = @selector(initWithCoder:);
        SEL pullViewInitSelector =  @selector(init__LM__Swizzled__WithCoder:);
        Method originalMethod = class_getInstanceMethod(self, initSelector);
        Method newMethod = class_getInstanceMethod(self, pullViewInitSelector);

        BOOL methodAdded = class_addMethod([self class],
                                           initSelector,
                                           method_getImplementation(newMethod),
                                           method_getTypeEncoding(newMethod));

        if (methodAdded) {
            class_replaceMethod([self class],
                                pullViewInitSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, newMethod);
        }
    });
}

- (instancetype)init__LM__Swizzled__WithCoder:(NSCoder *)coder {
    self = [self init__LM__Swizzled__WithCoder:coder];
    if (self != nil)
    {
        objc_setAssociatedObject(self, CachedKey, [NSNumber numberWithBool:NO], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        objc_setAssociatedObject(self, PullMenuKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return self;
}

- (LMPullMenuContainerViewController*)pullMenuContainerController {
    BOOL isCached = [objc_getAssociatedObject(self, CachedKey) boolValue];
    if (isCached) {
        return objc_getAssociatedObject(self, PullMenuKey);
    } else {
        return [self pullMenuParentOf:self];
    }
}

- (LMPullMenuContainerViewController *)pullMenuParentOf:(UIViewController *)controller {
    if (controller.parentViewController) {
        if ([controller.parentViewController isKindOfClass:[LMPullMenuContainerViewController class]]) {
            objc_setAssociatedObject(self, CachedKey, [NSNumber numberWithBool:YES], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
            objc_setAssociatedObject(self, PullMenuKey, controller.parentViewController, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
            return (LMPullMenuContainerViewController *)(controller.parentViewController);
        } else {
            return [self pullMenuParentOf:controller.parentViewController];
        }
    } else {
        objc_setAssociatedObject(self, CachedKey, [NSNumber numberWithBool:YES], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        objc_setAssociatedObject(self, PullMenuKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        return nil;
    }
}

For now I've resigned to setting the property manually where necessary.

Evan Cordell
  • 4,108
  • 2
  • 31
  • 47
  • What do you mean by the flag being *"attached to UIViewController, and not specific subclasses of UIViewController"*? The associated objects are attached to an instance of the (sub)class, not to a class. – Martin R Jun 21 '13 at 20:43
  • That's what I thought and expected, however, when I step through it I find that `isCached` returns `YES` before it's been set on specific subclasses (but after it's been set on other subclasses). – Evan Cordell Jun 21 '13 at 20:51
  • Perhaps unrelated, but shouldn't you cache the result also in the `return [self pullMenuParentOf:controller.parentViewController];` case? – Martin R Jun 21 '13 at 21:03
  • Nope, that's the same function (it's recursive). So if it ever gets there, it will eventually get back to one of the other cases that does cache the result – Evan Cordell Jun 21 '13 at 21:34
  • But I still don't understand the problem (it might be my fault, it is late in Germany now :-) What exactly means *"... returns YES before it's been set on specific subclasses.... "* ? Do you really mean subclasses or are you talking about parent/child view controllers? Sorry if this is a dumb question but I still don't get it. – Martin R Jun 21 '13 at 21:38
  • I cannot reproduce it here. I use your code and modify your custom class to normal `UIViewController`. The `isCached` return false at the first time on my two controller as it should be. – tia Jun 22 '13 at 01:17

1 Answers1

0

As it happens, the above code works just fine. My container controller was loading all of the controllers it manages when it was first initialized and not when the controllers were first displayed, so to me it looked as though the flag had been set before it should have been.

Evan Cordell
  • 4,108
  • 2
  • 31
  • 47