iOS Recipes - Matt Drance [81]
Using Core Data in your apps means you’ll be doing a certain amount of redundant work. Isolating that code in one place for easy reuse results in less effort and fewer careless bugs in each project.
Recipe 40 Store Data in a Category
Problem
Objective-C categories let you add new behavior to any existing class in the Cocoa frameworks. One thing you can’t do with a category, however, is declare stored instance variables and properties.
Solution
The Objective-C runtime allows us to add methods to any class—even classes we don’t own, like Apple’s—by declaring a category. The various drawing methods on NSString, for example, are category methods declared by UIKit in UIStringDrawing.h.
These categories simply add behavior to their corresponding classes. UIStringDrawing declares methods, but no category can introduce new storage—properties or instance variables that are created and retained, and ultimately released, in the class’s ‑dealloc method.
As of Mac OS X Snow Leopard and iPhone OS (now iOS) 3.1, that’s no longer true. A new feature of the Objective-C runtime, called associative references, lets us link two objects together using a very basic key-value format. With this feature, we can create the effect of a category that adds new storage to an existing class.
Consider UITouch as an example. Whether we’re writing a custom view, view controller, or gesture recognizer, it’s incredibly handy to know the original point of origin for a given touch source. That information, however, is lost after receiving ‑touchesBegan:withEvent. Our class can keep track of that, but once we begin managing multiple touch sources, it becomes difficult. Plus, any code we write to track this state is stuck in that view, view controller, or gesture recognizer—we have to port it over to any other class we write later. It makes much more sense for the touch object itself to keep track of its point of origin.[8]
A category on UITouch called PRPAdditions will handle this by declaring two methods: one for storing the initial point and another for fetching it within the coordinates of the requested view.
TouchOrigin/UITouch+PRPAdditions.h
@interface UITouch (PRPAdditions)
- (void)prp_saveOrigin;
- (CGPoint)prp_originInView:(UIView *)view;
@end
Remember, categories do not let us declare storage on the class they extend; only methods do. This is where associative references come in. We start by fetching the current touch’s location to global screen coordinates, which protects us against any changes to the view hierarchy that might occur over the touch object’s life span. We then store the point in an NSValue object and save that value to our UITouch instance as an associative reference using the objc_setAssociatedObject runtime function.
TouchOrigin/UITouch+PRPAdditions.m
- (void)prp_saveOrigin {
CGPoint windowPoint = [self locationInView:nil];
CGPoint screenPoint = [self.window convertPoint:windowPoint toWindow:nil];
objc_setAssociatedObject(self,
&nameKey,
[NSValue valueWithCGPoint:screenPoint],
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
It’s worth discussing the point conversion we do here prior to storing the value. Every UITouch object has a window property, which refers to the window where the touch began. We get the touch’s origin by sending [self locationInView:nil]. Per Apple’s documentation, this provides the location in the window’s coordinate space—we could also have passed self.window if we wanted. We then pass this point to ‑convertPoint:toWindow:, passing nil for the final parameter. The UIWindow reference explains that passing nil converts the point “to the logical coordinate system of the