Monkey-Patching iOS with Objective-C Categories Part II: Adding Instance Properties
Originally posted on Carbon Five’s Blog.
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 UINavigationController
or UITableViewController
? You would either have to create custom subclasses for each of them (TouringNavigationController
, 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.
UIViewController+TourGuide.h
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
From the Objective-C Programming Language Guide - Categories and Extensions:
Note that a category canât declare additional instance variables for the class; it includes only methods.
So the @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 -tourSteps
and -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 key
. The 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)
OBJC_ASSOCIATION_ASSIGN
- (strong) / (retain)
OBJC_ASSOCIATION_RETAIN
- (copy)
OBJC_ASSOCIATION_COPY
- (nonatomic,strong)
OBJC_ASSOCIATION_RETAIN_NONATOMIC
- (nonatomic,copy)
OBJC_ASSOCIATION_COPY_NONATOMIC
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.
Next
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.
TL;DR
- You can add properties through categories but NOT instance variables;
@syntesize
will fail. - Use Associated References to replicate the behavior of instance variables.
- The property is identified using a fixed memory address; define a
static char
variable whose address is the identifier. - Use the
objc_setAssociatedObject
to implement the setter, passing inself
as the object, the address of your key variable, the value of the property and the appropriate policy. - Use
objc_getAssociatedObject
to 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!