Ken M. Haggerty
Ken M. Haggerty

Reputation: 26198

Any way to apply Objective-C category only to current class (or equivalent effect)?

Let's say I have a custom subclass of UIView called MyCustomView. Let's also say that I have a category on UIView called UIView+Dictionary that adds an NSDictionary property called dictionary to every UIView.

If I were to import UIView+Dictionary.h into MyCustomView.m then every view referenced within MyCustomView.m would have this added dictionary property, which in many situations is exactly the desired behavior.

However, if I wanted UIView+Dictionary applied only to MyCustomView itself and not to every UIView referenced within MyCustomView.m, is there a way to do so (or achieve a similar effect)?

I'd like to avoid making MyCustomView a subclass of another custom subclass (e.g., MyViewWithDictionary), as I'd ideally like to be able to import multiple categories for something akin to multiple inheritance (e.g., UIView+Dictionary, UIView+Border, UIView+CustomAnimations).

In my actual own scenario, I've written a category to automatically implement a custom UINavigationBar in a view controller, but I'd like that category to apply only to the view controller into which I am importing the category and not any other view controllers that may be referenced in that file.

Any and all insights are appreciated! And I apologize in advance as I am fairly certain there are more correct terminologies for the effect described above.

Upvotes: 2

Views: 277

Answers (2)

Ken M. Haggerty
Ken M. Haggerty

Reputation: 26198

As Josh pointed out, any methods added in categories are basically inert unless you call them. The issue that I was having was for generated properties and swizzled methods in categories (since, as Josh also pointed out, there are no mixins in Objective-C).

I was able to solve this by adding in a custom BOOL in my category that defaults to NO and acts as a "switch" for whatever category methods and properties I want to specify.

E.g., if I wanted my dictionary property to be lazily instantiated but only within MyCustomView, I could do the following:

// UIView+Dictionary.h

@interface UIView (Dictionary)
@property (nonatomic) BOOL enableDictionary;
@property (nonatomic, strong) NSDictionary *dictionary;
@end

// UIView+Dictionary.m

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

@implementation UIView (Dictionary)

- (void)setEnableDictionary:(BOOL)enableDictionary {
    objc_setAssociatedObject(self, @selector(enableDictionary), @(enableDictionary), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (BOOL)enableDictionary {
    NSNumber *enableDictionaryValue = objc_getAssociatedObject(self, @selector(enableDictionary));
    if (enableDictionaryValue) {
        return enableDictionaryValue.boolValue;
    }

    objc_setAssociatedObject(self, @selector(enableDictionary), @NO, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    return self.enableDictionary;
}

- (void)setDictionary:(NSDictionary *)dictionary {
    objc_setAssociatedObject(self, @selector(dictionary), dictionary, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSDictionary *)dictionary {
    if (!self.enableDictionary) {
        return nil;
    }

    NSDictionary *dictionary = objc_getAssociatedObject(self, @selector(dictionary));
    if (dictionary) {
        return dictionary;
    }

    objc_setAssociatedObject(self, @selector(dictionary), @{}, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    return self.dictionary;
}

@end

And then within -[MyCustomView viewDidLoad] I could simply call self.enableDictionary = YES. That way, only instances of MyCustomView will have a non-nil lazily instantiated NSDictionary. (Note that, in this example, all instances of UIViews will still respond to the selector @selector(dictionary), but our behavior will differ based on whether enableDictionary is YES or NO.)

While that is a trivial example, the same strategy can be used for methods that are swizzled within categories. (Again, swizzling methods within categories is probably bad form but a necessary evil in certain scenarios.)

Upvotes: 0

jscs
jscs

Reputation: 64002

However, if I wanted UIView+Dictionary applied only to MyCustomView itself [...] is there a way to do so [...]?

Only by changing the category to be on MyCustomView and not UIView.

The header has nothing to do with whether the category's methods are present on any given instance. If the category is compiled into your program, the methods are there, no matter where the instance is created. This is the reason that prefixes are so important on methods that are added to framework classes: categories have global effect, and name collisions are undefined behavior.

The header only affects the visibility of the methods as far as the compiler is concerned. You can use the usual tricks to call them at runtime regardless.

The category takes effect on the class itself, when the runtime is initialized at launch. If you want the methods of the category to be available only on a certain class, the category must be defined on that class.

Upvotes: 2

Related Questions