Add variables to an existing class in objective-c
Categories are a great way to add functionality to existing classes in Objective-C without extending them.
Say you want to add a method to MD5 hash a particular string. A Java way of doing this would be to create your own string class by subclassing NSString and add the hashMD5 method to it. With Objective-C, you don't have to subclass at all, you can simply write a category on NSString that declares the hashMD5 method. Then, you'll be able to call the hashMD5 method on any NSString instance, assuming you correctly imported the category header. Categories are very elegant and powerful, however there is a drawback with them: you cannot add instance variables! The interface/implementation of a category looks like following:
#import <Foundation/Foundation.h>
@interface NSString (StringAdditions)
@end
#import "NSString+StringAdditions.h"
@implementation NSString (StringAdditions)
@end
As you can see, there is no room for instance variables! So you think you'll maybe be able to add a property to it. You can indeed declare the property in the interface with
@property (nonatomic,retain) NSString *coolString ;
but if you try to synthesize it in the implementation, you will get an error "@synthesize not allowed in a category implementation". The problem is that @synthesize will create a getter and setter for us but by doing that, it will also have to create a instance variable of the same name where to store the value and get it from, hence the problem.
However, starting with Snow Leopard and iOS 3.1, the objective-C runtime adds Associated Object support. It might sound a bit of an obscure term but it literally adds some magic! From the Apple runtime documentation, it allows you to "Returns the value associated with a given object for a given key" and "Sets an associated value for a given object using a given key and association policy". Let see it with an example: assume you want to set a default string that your hashMD5 method would return in case it cannot compute the hash (not very pertinent example, sorry about that ;)). You would then define the retained defaultHash string property as following:
#import <Foundation/Foundation.h>
@interface NSString (StringAdditions)
@property (nonatomic,retain) NSString *defaultHash ;
- (void)printString ;
@end
As we've seen earlier, you won't be able to @synthesize this property in your implementation so let's write the getter and setter ourselves. You need to make sure that you correctly import the objective-c runtime header.
#import "NSString+StringAdditions.h"
#import <objc/runtime.h>
@implementation NSString (StringAdditions)
static char defaultHashKey ;
- (NSString *)defaultHash
{
return objc_getAssociatedObject(self, &defaultHashKey) ;
}
- (void)setDefaultHash:(NSString *)aString
{
objc_setAssociatedObject(self, &defaultHashKey, aString, OBJC_ASSOCIATION_RETAIN_NONATOMIC) ;
}
- (void)printString
{
NSLog(@"the string is: %@", defaultHash) ;
}
@end
So, let's see what we've done here. We first declare a static char which address will act as the (constant) key to our variable. We then defined the getter defaultHash and setter setDefaultHash:. In the getter, we simply return the object associated with our object (in this case self which is an instance of NSString) and the address of the key. Similarly, in the setter we set the object associated with our object, the given key (again the constant address of the char we declared earlier) and the memory policy that will match what you declared in the property (in our case nonatomic and retain). If you have a look at the runtime header, you can see that the following is defined, allowing you to match every kind of memory policy you are used to use in you properties.
enum {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};
typedef uintptr_t objc_AssociationPolicy;
If you now try to do the following in another class in you project
#import "NSString+StringAdditions.h"
- (void)test
{
NSString *string = [NSString string] ;
[string setDefaultHash: @"Ciao"] ;
[string printString] ;
}
you should see the following printed in the console: the string is: Ciao. Magical!
As a bonus, here are the function declaration from the runtime header
OBJC_EXPORT void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);
OBJC_EXPORT id objc_getAssociatedObject(id object, void *key)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);
OBJC_EXPORT void objc_removeAssociatedObjects(id object)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);
Hope that helps ;)