I came across something peculiar in a coworker’s custom Core Data entity class recently: boilerplate code for getter/setter methods that looked like it had been generated by Xcode, but was based on scalar rather than object values, like so:
- (NSInteger)age; - (NSInteger)age { [self willAccessValueForKey:@"age"]; NSNumber *result = [self primitiveAge]; [self didAccessValueForKey:@"age"]; return [result integerValue]; }
He swears this boilerplate code is untouched Xcode-generated code, but every Xcode I tried only generated object-based code. (Spoiler: he doesn’t remember, but I think he must have written it himself, like Apple recommends.)
Xcode doesn’t even generate object-based boilerplate anymore by default. As of 10.5 and up, the runtime will create object-based getter/setter methods for you, so Xcode only generates property stubs, like this:
@property (nonatomic, retain) NSNumber *age; @dynamic age;
Very nice, but Xcode 4.2 has a curious new checkbox called “Use scalar properties” in the class generation sheet:
When you check it, the new property declaration/definition is indeed scalar-based:
@property (nonatomic, retain) uint16_t age; @dynamic age;
Could I use this to replace the mysterious boilerplate in my coworker’s class? Well….
If you build and run this, it’ll work just fine sometimes. But some people have reported problems with it. (Can’t find the link right now, sorry.) The trick is that this new ability to generate scalar-based getter/setters at runtime is limited to iOS 5 and Mac OS X 10.7 “Lion”, and won’t work on earlier versions of those OSes. (To make matters worse, Apple’s documentation hasn’t been updated to mention this scalar functionality at all.)
Hm…I want the benefits of runtime-created getter/setters, which will almost certainly be more efficient than manually boxing and unboxing NSNumber objects in my own code…but I also want to support the older OSes. What’s a programmer to do?
mogenerator to the rescue!
mogenerator is…well, I’ll just put in the “elevator pitch” from the website:
mogenerator is a command-line tool that, given an .xcdatamodel file, will generate two classes per entity. The first class, _MyEntity, is intended solely for machine consumption and will be continuously overwritten to stay in sync with your data model. The second class, MyEntity, subclasses _MyEntity, won’t ever be overwritten and is a great place to put your custom logic.
mogenerator’s default template declares object-based getter/setters and also declares a second set of fully-implemented scalar-based getter/setters. For example, if the object-based accessor is age
, then the the scalar-based getter is ageValue
.
But I want something a little different: I want a scalar-based definition of age
and setAge:
, but only if the OS is earlier than 5.0/10.7. If not, I want no definition, so that the OS will generate it for me.
How do I do that? Here’s a partial example of a mogenerator-generated “machine” entity source code file, using my custom template:
#import <objc/runtime.h> #import "OSVersion.h" @implementation _Measurements @dynamic age; static short ageIMP(id self, SEL _cmd) { [self willAccessValueForKey:@"age"]; NSNumber *result = [self primitiveAge]; [self didAccessValueForKey:@"age"]; return [result shortValue]; } + (BOOL)resolveInstanceMethod:(SEL)selector { if ([OSVersion isLionOrGreater] == NO) { if (selector == @selector(age)) { class_addMethod([self class], selector, (IMP)ageIMP, [[NSString stringWithFormat:@"%s@:", @encode(short)] UTF8String]); return YES; } return [super resolveInstanceMethod:selector]; }
There are two tricks here. First is the use of resolveInstanceMethod:
to add the method implementation, stashed in the otherwise hidden ageIMP()
function. The code here is straight out of Apple’s documentation, so take a look there for details.
Second is the use of the custom class OSVersion
, which conveniently tells us which OS version we’re running on. That class is included in my mogenerator-Sample project on github (my first!).
Note: my github project’s sample app uses scalar getter/setters via a rather impractical UI, because for simple projects, object-based methods really fit better with Cocoa design patterns. For example, an object must be used as the value of an NSTableView cell. It’s only when you have more custom UI (or none) that scalar methods start making sense.
The most important detail, however, is that isLionOrGreater
must be fast, because it is called dozens of times for even one simple class. In my first implementation, which wasn’t fast, I managed to slow down our iPad app by about 30 seconds (!).