Saturday, September 11, 2010

CLLocation getDistanceFrom: vs distanceFromLocation:

In Backwards Compatibility if Apple Starts Polishing, Oliver Drobnik explains how he solved the getDistanceFrom: vs distanceFromLocation: problem. His solution involves a new method (distanceBetweenLocation:andLocation:) and eight lines of code for calling getDistanceFrom: through a NSInvocation.

Wouldn't it be nice if instead you could use [aLocation distanceFromLocation:anotherLocation] everywhere in your code, while still retaining backward compatibility? Enter the Objective-C runtime! In your main.m file, first #import <objc/runtime.h> and at the very beginning of your main function, add this:


Method getDistanceFrom = class_getInstanceMethod([CLLocation class], @selector(getDistanceFrom:));
class_addMethod([CLLocation class], @selector(distanceFromLocation:), method_getImplementation(getDistanceFrom), method_getTypeEncoding(getDistanceFrom));


Here you go, -[CLLocation distanceFromLocation:] available at runtime in any iOS version with only two lines of code. Note that on iOS 3, class_addMethod will add the distanceFromLocation: method to the CLLocation class using getDistanceFrom: implementation. On iOS 4, class_addMethod will do nothing as the method already exists.

12 comments:

Peter said...

thumbs up for a simple and effective solution

Stephen Connolly said...

I'm getting the following errors:

error: initializer element is not constant

error: expected declaration specifiers or '...' before '[' token

error: expected declaration specifiers or '...' before 'selector'

error: expected declaration specifiers or '...' before 'method_getImplementation'

error: expected declaration specifiers or '...' before 'method_getTypeEncoding'

warning: data definition has no type or storage class

warning: type defaults to 'int' in declaration of 'class_addMethod'

error: conflicting types for 'class_addMethod'

Any thoughts ?

Cédric Luthi said...

If your main file is main.c, rename it to main.m and make sure you #import both <objc/runtime.h> and <CoreLocation/CLLocation.h>

Stephen Connolly said...

Cédric,

Sorry, I did't read your instructions correctly initially. I've made sure my main file is main.m and I have included both #import statements, but still getting the errors listed.

Stephen

Cédric Luthi said...

At the very beginning of your main function means just after int main (int argc, const char * argv[]) {

Stephen Connolly said...

Ah yes, that solved it, thanks very much.

Colin said...

Excellent stuff, very nice indeed!

Anonymous said...

Shouldn't this be placed inside the pool?

NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

Anonymous said...

How likely/unlikely will this cause problems in iOS 5.0 or 6.0 etc?

Cédric Luthi said...

No, there is no need to place it inside the pool. These two lines will never trigger an autorelease message to be sent.

This will not cause any problem on any future iOS that already has the distanceFromLocation: method.

Stephen Connolly said...

Hello Cédric,

I'm only getting around to testing this now. I've implemented your code as below:

main.m

#import
#import
#import


int main(int argc, char *argv[])
{
Method getDistanceFrom = class_getInstanceMethod([CLLocation class], @selector(getDistanceFrom:));
class_addMethod([CLLocation class], @selector(distanceFromLocation:), method_getImplementation(getDistanceFrom), method_getTypeEncoding(getDistanceFrom));

NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
int retVal = UIApplicationMain(argc, argv, nil, nil);
[pool release];
return retVal;
}

and in my ViewController I have the following line:

CLLocationDistance meters = [newLocation distanceFromLocation: oldLocation];

but this gives me an error 'incompatible types in initialization'. Any thoughts, am I missing something ?

Regards,
Stephen

Stephen Connolly said...
This comment has been removed by the author.