13

Before iOS 4, I used to add a observer to each MKAnnotationView added to the map view, listening for it's selected method, so I know when the user has tapped on a pin.

This worked fine up to iOS 4.2. I've noticed on the release noted annotation views are actually being reused and it somehow messes up with the observers.

So I figure I can use the -mapview:didSelectAnnotationView: method from MKMapViewDelegate for my needs, but that has only been added to iOS 4.0 SDK.

So, to maintain compatibility, I'd like to implement this method on my delegate and conditionally check for the presence of this method on the MKMapViewDelegate protocol so that if it's not present, I will add my observer to the annotation view.

How can I do this for a protocol method, similarly for how we check if a class is not nil?

UPDATE:

As Daniel Dickison pointed out, I can't use respondsToSelector:, because my delegate has -mapview:didSelectAnnotationView: implemented for 4.0+ devices. What I need is to check if the protocol on that device has the optional -mapview:didSelectAnnotationView: method OR if MKMapView will look for that method on it's delegate.

I ended up doing a test for the current iOS version running. If it's higher than 4.0, MKMapView will look for that method and call it.

if ([[[UIDevice currentDevice] systemVersion] doubleValue] < 4.0)
    [self setupObserver];

This solves the original problem, but it would still be interesting to check the actual protocol for the method somehow.

leolobato
  • 2,359
  • 3
  • 32
  • 51

5 Answers5

26

Because there is no object instance you can ask if it responds to a message selector, and you already know the protocol is supported but you are just looking for one method within - you need to use protocol_getMethodDescription, like so (method is class instance and optional) where you check for a nil return value:

#import <objc/runtime.h>

struct objc_method_description hasMethod = protocol_getMethodDescription(@protocol(MKMapViewDelegate), @selector(mapView:didSelectAnnotationView:), NO, YES);

if ( hasMethod.name != NULL )
{
...
}
Daniel Dickison
  • 21,832
  • 13
  • 69
  • 89
Kendall Helmstetter Gelner
  • 74,769
  • 26
  • 128
  • 150
  • Shouldn't the protocol be referenced as `@protocol(MKMapViewDelegate)`? In any case, I have a feeling this will use the MKMapViewDelegate that is provided by the SDK rather than the runtime. – Daniel Dickison Nov 25 '10 at 14:55
  • You are right about the @protocol (I fixed the code) but since the check is runtime, it will look at the protocol that exists at the time the application is run. Probably also, the MKMapKit framework should be weak-linked (On the app target do "Get Info", then in the General tab look down at the list of frameworks and change MapKit from "required" to "weak"). – Kendall Helmstetter Gelner Nov 26 '10 at 08:01
  • Check out the last two paragraphs of the "Protocol Objects" section in this document -- I think it implies that you *may* get the protocol as defined by the SDK instead of the runtime. http://developer.apple.com/library/ios/#documentation/Cocoa/Conceptual/ObjectiveC/Articles/ocProtocols.html#//apple_ref/doc/uid/TP30001163-CH15-TPXREF149 – Daniel Dickison Nov 28 '10 at 03:29
  • "The compiler creates a Protocol object for each protocol declaration" - that means the compiler creates an instance of that protocol class, but it can only create one for a protocol object that exists in the framework. If you weak link Mapkit at runtime the only possible protocol object you can get is the one the system provides. – Kendall Helmstetter Gelner Nov 29 '10 at 19:29
  • To me the doc sounds like the compiler will *not* create a Protocol object *unless* it's referenced with @protocol or by a class adopting it. If it's only used in type declarations (e.g. id) then it won't create one, which leaves the possibility that the runtime framework does not contain the protocol object, in which case the one created by the app compiler will be used. Having said that, I just tested this with an original iPhone running iOS 3.1.3, and `protocol_getMethodDescription` does return NULLs in this case, but I don't think it's guaranteed to work. – Daniel Dickison Nov 30 '10 at 14:10
  • An example where this doesn't work is if you weak-link `iAd.framework`, which does not exist at all in iOS 3.1.3. If you test for methods on `@protocol(ADBannerViewDelegate)` using `protocol_getMethodDescription`, you *will* get method descriptions back -- that's (presumably) because the compiler created its own version of the protocol object while building the app. – Daniel Dickison Nov 30 '10 at 14:13
  • 1
    Right, if the framework isn't there you have an issue, but we are talking about a special case where the framework is know to exist on all systems you can run on, but only one method is missing. Otherwise you could just check to see if the protocol exists and be done with it. It is guaranteed to work, by the simply fact that as I said it is not possible to create a MapKitDelegate protocol object other than the one held in the system MapKit framework where your app is running. – Kendall Helmstetter Gelner Dec 01 '10 at 04:17
5

That's a tricky one. So if I'm understanding your question correctly, you want to find out, at runtime, whether the map view sends the mapView:didSelectAnnotationView: message to its delegate. But you can't use conformsToProtocol: or respondsToSelector: because you're implementing the delegate so obviously you're adopting the protocol and implementing the method.

The only thing I can think of is to check for some other method that was added to MKMapView (not the delegate) in iOS 4, like: mapRectThatFits:.

Another possibility is to use the Objective-C runtime library to query the Protocol object. But this is probably overkill, and also I don't think it will work because the Protocol object is created by the compiler when you build your app, and it's possible you'll get the UIKit SDK-defined MKMapViewDelegate protocol object instead of whatever the runtime was compiled with.

Daniel Dickison
  • 21,832
  • 13
  • 69
  • 89
3

I think you want NSObject conformsToProtocol - something like:

BOOL test = [myObject conformsToProtocol:@protocol(MKMapViewDelegate)];
Eric
  • 3,865
  • 4
  • 32
  • 44
2

I would use the respondsToSelector: method because that allows you to check for specific methods (which it sounds like you're doing, otherwise, if you're looking to check for a specific protocol, @Eric's answer is a good one). This SO post talks about using it this way.

Basically, the way you'd use it is

SEL methodName = @selector(mymethod:);
BOOL test = [object respondsToSelector:methodName];
Community
  • 1
  • 1
Chris Thompson
  • 35,167
  • 12
  • 80
  • 109
  • 1
    Chris - you may not want to use the reserved keyword select as the SEL variable name. – DHamrick Nov 24 '10 at 21:34
  • @DHamrick, you are absolutely right...so dumb...I wrote it quickly and had a face-palm... – Chris Thompson Nov 24 '10 at 23:02
  • 1
    You can't use "respondsToSelector" because the only object you would ask it of is yourself! You need to find out if the MKMapView will be looking for that method from a delegate... – Kendall Helmstetter Gelner Nov 24 '10 at 23:14
  • Not necessarily, if this object is implementing a protocol that may or may not have this method there, you may want your generic code to be totally agnostic to what your delegate has implemented. – Chris Thompson Nov 24 '10 at 23:16
  • You are not making any sense. The only object that will ever implement the delegate method is one you write - there are no system MKMapView delegates. That is your own controller! You already KNOW it implements this method. What the questioner wanted to know was IF THIS METHOD WOULD BE CALLED. You can ONLY find that out by looking at what the protocol contains, because the MKMapView is the one who will be calling "respondsToSelector" on your own code! – Kendall Helmstetter Gelner Nov 24 '10 at 23:47
  • Fair enough, I can see what you're saying. I reread the question and understand what you're saying. I can see an instance where it would make sense to check if a method was implemented, for instance if it was conditionally implemented with a `#define` wherein the code doesn't even appear if the iOS version isn't correct, but that isn't what the OP is asking. – Chris Thompson Nov 24 '10 at 23:51
0

I've taken a slightly different approach.

I simply use an #ifdef (__iPHONE_OS_VERSION_MIN_REQUIRED... and add observer if necessary, along with using the -mapview:didSelectAnnotationView: delegate method.

joshpaul
  • 953
  • 8
  • 12
  • "#ifdef (__iPHONE_OS_VERSION_MIN_REQUIRED" will be resolved at build time, not runtime, so once you build your app it's always going to do the same thing not matter what device/OS it is running on. – Jon Hess Nov 25 '10 at 02:51
  • This doesn't work. You need to know the iOS version of the Device at runtime, not at build time. `__iPHONE_OS_VERSION_MIN_REQUIRED` will always return the value you specified on build settings, not matter what Device/iOS version you're running it on. – leolobato Nov 25 '10 at 13:16
  • That was the approach I took. Your requirements may be different. How often are you releasing? Is your project a moving target? How many devs are on your team? Again, it's simply the approach I took (which may or may not have inspired your update). – joshpaul Nov 25 '10 at 19:12