Adding missing framework methods at runtime
December 14, 2011

Let’s say that you are building with a modern SDK, but you are targeting some older platforms. For example, you’re using the iOS 5.0 SDK, but you want your app to run on iOS 4.2.

When you do this, you have to take care not to call routines which are only implemented on iOS 5. You can set your deployment target to 4.2, but that won’t prevent you from calling 5-only routines, they will simply be weak linked and will be nil on platforms that don’t support them.

Occasionally I slip up on this, and during testing I discover that I’m using a routine that doesn’t exist everywhere.

The simple solution when this happens is to stop using the routine in question, and find some workaround, but that’s a bit rubbish. After all, the routine has probably been added precisely because it’s useful.

Also, the routine, when it does exist, might be better than your workaround. Apple wrote it, so it must be good right? So you’d prefer to only use the workaround where necessary.

Finally, this is a temporary problem that will probably go away at some point. The routine is present on new systems, and eventually time will move on and you’ll stop supporting the ones where it is missing.

So rather than changing your code, how about finding a way to add the missing implementation when necessary? Luckily, Objective C is wonderfully dynamic, so this is entirely feasible. Here’s a little example taken from ECFoundation.

UIColor has a method getRed:green:blue:alpha: which only seems to be present in iOS 5 (although the header helpfully fails to mention this). How can we arrange to add an implementation for it, if it’s missing?

The first thing we want to do is add the replacement implementation. We can put this anywhere really, but for neatness it might as well go into a private category on the class we’re fixing up. The actual code here doesn’t really matter for the purposes of the example - the point is that you’ve now got a method implementation that you can install if you need to:

- (BOOL)ecGetRed:(CGFloat *)red green:(CGFloat *)green blue:(CGFloat *)blue alpha:(CGFloat *)alpha
{
	const CGFloat* components = CGColorGetComponents(self.CGColor);
	BOOL ok = components != nil;
	if (ok)
	{
		*red = components[0];
		*green = components[1];
		*blue = components[2];
		*alpha = components[3];
	}
	
	return ok;
}

So how to we do the installation? We only want to do it once, and we want to do it before the routine is called, otherwise it won’t be much use.

This sounds like a job for the +initialize method, but there’s a slight problem. What happens if you add an initialize method in a category and there’s already one defined in the system elsewhere. Presumably you get a name clash and one version won’t get called. Nasty.

Luckily, there’s also the +load method. This gets called super early, and has some interesting properties. Here’s a quote from a great Mike Ash article:

An interesting feature of +load is that it’s special-cased by the runtime to be invoked in categories which implement it as well as the main class. This means that if you implement +load in a class and in a category on that class, both will be called. This probably goes against everything you know about how categories work, but that’s because +load is not a normal method. This feature means that +load is an excellent place to do evil things like method swizzling.

What a piece of luck! The system frameworks will be loaded by the time it runs, so we can test for the routine’s presence, and if it’s not there, add it:

+(void)load
{
	SEL apiSelector = @selector(getRed:green:blue:alpha:);
	if (![self instancesRespondToSelector:apiSelector])
	{
		SEL replacementSelector = @selector(ecGetRed:green:blue:alpha:);
		Method replacementMethod = class_getInstanceMethod(self, replacementSelector);
		IMP replacementImplementation = method_getImplementation(replacementMethod);
		const char* encoding = method_getTypeEncoding(replacementMethod);
		class_addMethod(self, apiSelector, replacementImplementation, encoding);
	}
}

[Updated Feb 2012 to fix some github links that have changed]

« Radar: Why Closed Bug Reporting Sucks Ambientweet 1.0.2 Released »
Got a comment on this post? Let me know at @samdeane@mastodon.org.uk.