Have you ever wanted to introduce new functionality to base classes in the iOS SDK?
Or just make them work a little differently? In order to do so, you must enter the wild and dangerous world of monkey-patching.
In this series of posts, we’ll show how to monkey-patch in Objective-C through categories to add and change methods, to add new instance variables and properties, and introduce swizzling, a technique that allows us to extend and preserve existing functionality. TL;DR »
In the first post we showed how you can add or override methods with extensions. In this post we’ll cover how to add new properties to instances.
The Scenario : Adding a New Feature
Why would we want want to patch in a new property to our class hierarchy instead of using subclassing?
Let’s imagine we are creating an app with multiple controllers and wish to add a “touring” feature; the first time a user arrives on a screen, popup tips appear to guide them through its functionality. It would make sense to add a
tourSteps property to our controllers, which they each set with their own unique tour.
We could add this property through subclassing
UIViewController; introducing a
TouringViewController for example that our controllers would then extend. But what if want the functionality of other core iOS controllers like
UITableViewController? You would either have to create custom subclasses for each of them (
TouringTableViewController, etc.) which your own controllers would then extend, or abandon using them and reimplement their functionality. Neither solution is appealing.
Instead, use categories to inject the new property into
UIViewController and have it available to all descendents, whether our own or
the iOS framework. As when defining properties for regular classes, the
@property declarative is used as shorthand to define the getters and setters of a property in the category header file.
The usual “next step” after defining a property is to use a
@syntesize declarative in the implementation to create the expected getter and setter methods and back them with an appropriate instance variable. We would expect the same to work when adding a property via a category:
However, the above code will fail. Why?
The Problem: No Instance Variables
Note that a category can’t declare additional instance variables for the class; it includes only methods.
@syntesize cannot create a
\_tourSteps instance variable to back the generated getter and setter. Likewise defining the instance variable in the category header file as follows would still not work:
Non-legal Declaration of Instance Variables in Category
What are our options? We clearly have to implement the getter method
-tourSteps and setter method
-setTourSteps: ourselves, but where will we store the actual values if not an instance variable? A
static variable? Doing so at the class-level makes no sense as each instance needs its own value, and we face memory retention headaches if we create them at the method level.
We could maintain a global mapping of objects to their per-instance property values but it would be difficult to correctly manage memory for that collection and properly clean up variables when their associated instance is dealocated.
Luckily the Objective-C runtime already provides such a global mapping for us, handling the memory management issues as long as we use it properly.
The Solution: Associated References
Associated Reference are provided through a collection of Objective-C runtime functions to simulate the behavior of instance variables. Through them you can create and set associations between your class instance and objects that represent their property values. More importantly, those associations are released automatically when your objects are released.
Using associated references, our implementation of
-setTourSteps: is as follows:
Let’s walk through what is happening here.
The “Key” to Creating Associations
The functions for getting and setting associations refer to an
object and a
object value is the instance that owns the property. In our case, it is
self. We then identify the property we will try to retrieve with the
key. But what is the key?
Unlike most mapping systems, it is NOT a string; it’s a fixed address in memory, hence the pointer in the method signature. It needs to be fixed to ensure we are always using the same key value when retrieving a specific property. A
static variable fits this criteria perfectly. And since the address is all we care about (retrieved with the address (
&) operator), what is in that memory address doesn’t matter at all. We make it a
char to minimize its footprint. So we define the key OUTSIDE the class as:
static char tourStepsKey;
And later use
&tourStepsKeys when we set or get the value.
Respecting Property Attributes When Setting Values
The methods we create an association with is the
objc_setAssociatedObject runtime function:
void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)
We make use of it in our property setter implementation, passing in
self and the address of
tourStepsKey as previously discussed. The third parameter is literally the
value we are setting the property to. Note that it is a
id reference, meaning we cannot pass primitive values (like
NSInteger) but only objects (like
NSNumber). Keep this in mind when defining and implementing your own properties.
The final parameter to
objc_setAssociatedObject is the
policy. It clues the runtime on what to do with the values when their associated object is removed from memory, corresponding to the property attributes strong, weak, copy, and so on. When implementing instance properties, simply pass in the appropriate policy:
- (weak) / (assign)
- (strong) / (retain)
You can also remove an association using
objc_setAssociatedObject by passing
nil as the
value. This works perfectly for our purposes of implementing the behavior of properties.
Getting the Associated Values
Retrieving our property is even easier with the
objc_getAssociatedObject runtime method:
id objc_getAssociatedObject(id object, void *key)
Like when the value was set, we pass in
self and the address of our property identifying key variable to the method. Note we return the value right away instead of casting it; again, as it is an
id reference, no type will be enforced by the compiler.
So there you have it; while you can’t technically have instance variables backing category defined properties, through the use of Associated References you can implement their functionality rather easily.
In the next and final post in the series, we will see how to use the technique of swizzling to accomplish something we were warned off last time; truly overriding and extending existing methods, even those core to the operation of iOS.
- You can add properties through categories but NOT instance variables;
- Use Associated References to replicate the behavior of instance variables.
- The property is identified using a fixed memory address; define a
static charvariable whose address is the identifier.
- Use the
objc_setAssociatedObjectto implement the setter, passing in
selfas the object, the address of your key variable, the value of the property and the appropriate policy.
objc_getAssociatedObjectto implement the getter.
- Check out a related bonus trick below!
BONUS: The Exception to the Rule
There is ONE exception to the rule of categories being unable to define instance variables using
@synthesize for properties defined in the category; when it’s a [class extension][categores]. For example, the code below defines a property
foo in an extension, which appears as an “anonymous” category with empty () paratheses:
But why is this useful? Well when the extension is declared not in an header file but with immediately with the implementation it effectively provides for “private” properties. The property will not be available to other components that reference the header, but you can within the implementation and without the hassle of using associated references! We can also use this technique to have a property defined with one set of attributes in its header file (for example
readonly) and then redefine it with a different set of attributes in extension only available to the implementation!