So you’ve used MKOverlayView to add a Google Map to your app and you’ve added to it a series of MKOverlays and implemented mapView:viewForOverlay: which returns the corresponding MKOverlayView for your overlay. But the problem is that now you want to detect touch events on the MKOverlayView so that an action can be taken depending on which view is tapped.
This seems to be one of those questions that many have asked, but has not really received any successful answer! Well, there is actually a way to detect touches on an MKOverlayView, and it’s surprisingly simple!
The first thing you need to do is set up an NSMutableArray as an ivar to store all MKOverlayViews currently visible on the screen. The simplest thing to do here would be would be to to just store every MKOverlayView you create in mapView:viewForOverlay: in this array. The problem is that if you keep adding (and never removing) overlays as the map moves, your array will keep growing and growing which may cause performance issues in what comes later.
So now you have an ivar (let’s call it overlayViews) which contains all the MKOverlayViews currently visible on the map (it may also contain other MKOverlayViews that are no longer visible if you go with the simple approach).
Next thing you need to do is create a UIGestureRecognizer and add it to the MKMapView (which I assume is called mapView). You can do this in viewDidLoad. Simply add the following three lines:
UIGestureRecognizer *gestureRecognizer = [[GestureRecognizer alloc] init];
gestureRecognizer.delegate = self;
[mapView addGestureRecognizer:gestureRecognizer];
UIGestureRecognizer is a class used to handle touch gestures, and by setting the delegate to self and adding it as a gesture recogniser to the MKMapView, which means that all touch events will now fire a delegate method on self.
This would be a good time to modify your header file to implement UIGestureRecognizerDelegate. While you’re there, declare a new ivar called dragging of type BOOL – we’ll explain why in a moment. Back in your main file, you can now implement the UIGestureRecognizerDelegate methods:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
dragging = NO;
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
dragging = YES;
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if (!dragging) {
if ([touches count] == 1) {
UITouch *touch = [touches anyObject];
for (MKOverlayView *overlayView in overlayViews) { // this is where things can get inefficient if you have loads of overlayViews that aren't even visible!
CGSize overlayViewSize = overlayView.frame.size;
CGPoint touchPoint = [touch locationInView:overlayView];
if (touchPoint.x > 0 && touchPoint.x < overlayViewSize.width && touchPoint.y > 0 && touchPoint.y < overlayViewSize.height) {
[self overlayTouched:overlayView]; // overlayView is the one that was touched!
}
}
}
}
}
Let’s go through this step-by-step. Firstly, why did we need the dragging BOOL? Well, not only do you want to be able to handle touch events for the MKOverlayView, you also will likely want to still allow the user to scroll and pinch-zoom on the map. If you simply directed all touch events to the MKOverlayView, you would end up with a situation that even touching and dragging at a point on the map over an MKOverlayView would result in a touch event being triggered on the MKOverlayView, which is probably not what you wanted. Therefore, the dragging boolean keeps track of whether the user is dragging their finger across the screen, and if so, the touch event is ignored and is just handled by the MKMapView. That’s why in touchesBegin:withEvent: we set dragging = NO, and when touchesMoved:withEvent: we update dragging = YES. If touchesBegin:withEvent: is followed immediately by touchesEnded:withEvent: then dragging remains NO and we know that it was a tap, rather than a drag.
So once we know that the touch event is indeed a tap and not a drag, we then iterate over all of the MKOverlayViews that we stored previously in overlayViews, and for each one we compare the coordinates of the touch event against the coordinates of the overlayView. If the touch event lies within the frame of the overlayView then we know that that is the MKOverlayView that was touched, and can take action appropriately.
That action could be handled in the class itself, or you might choose to subclass MKOverlayView so that the view can handle its own events, and then call a method on it (e.g. [overlayView touched]) where appropriate.
[...] Originally Posted by vertine in fact, no. i never did- would still love to know this one Solved: Detecting touches on MKOverlayView Jonathan Ellis [...]
Thanks a lot for your very understandable explanation. It really helped me a lot while i was looking for a solution long.
Could you please tell me how to store MKOverlayViews currently visible on the screen?
Keerthi, thanks for your message. Regarding your question, the simplest (and most naive) way would, as I mentioned, to create an
NSMutableArrayas an instance variable and keep adding to it whenever you create a new view inmapView:viewForOverlay:.Obviously this is inefficient because you will keep adding more and more views without ever removing any. Eventually, as the user pans around the map, you could end up clogging up the array with hundreds or even thousands of overlays and end up crashing the app.
One way around this would be to set-up a timer that periodically goes through the array and checks which views are no longer visible and then removes them from the array.
However, there are much more clever ways of doing it. One way is to look at how
UITableViewhandles storingUITableViewCells. If you’ve ever used that part of the SDK, you will know that the table view “dequeues” existing cells and re-uses them wherever possible in order to make efficient use of memory.It should be possible to apply exactly the same strategy here in order to recycle the
MKOverlayViews so that memory is used most efficiently and the code runs as quickly as possible. This might be something I address in a future blog post. If in the meantime you work out a solution to this then please do post it here!this is not work form me.
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
not fire. can you send me the hold sample project to email:topgiftie@gmail.com
hi, I have a same question, it is not work form me.
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
this functions not fire, can u help me ?
These methods are NOT UIGestureRecognizerDelegate methods:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
The ONLY UIGestureRecognizerDelegate methods are:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
(See Apple’s doco at: http://developer.apple.com/library/ios/#DOCUMENTATION/UIKit/Reference/UIGestureRecognizerDelegate_Protocol/Reference/Reference.html )
I don’t think the UIGestureRecognizer is actually of any use in this case, and is just confusing the issue.
The three methods you’ve listed are methods built into the UIResponder class (see http://developer.apple.com/library/ios/#documentation/uikit/reference/UIResponder_Class/Reference/Reference.html). This means they are built into every UIView class and subclass (which inherits from UIResponder).
PS. The UIResponder documentation also states that, “If you override [a touch-handling] method without calling super (a common use pattern), you must also override the other methods for handling touch events, if only as stub (empy) implementations.” Which menas that you should also override the touchesCancelled:withEvent: method.
PPS. For those people who can’t get the touchesXxxx:withEvent: methods to work, it may be because you’re using them as delegate methods (which they are not) and not including them in a MKMapView subclass (or at least in a UIView subclass) which is where they need to be.
Son of a Beach,
You are 100% correct — I did indeed make a mistake with the
UIGestureRecognizercode, and didn’t include an important step. In fact, it is necessary to subclassUIGestureRecognizerin order to get the correct functionality, and forward thetouches...events properly.That’s where the confusion comes from, and I suspect most of the issues posted in these comments. I will amend the post later on today.
Thanks for pointing that out.
Thanks for the auspicious writeup. It actually was a amusement account it. Look complex to far introduced agreeable from you! By the way, how can we communicate?
I used your solution thanking you for that but I bounced in crash:
-[MKPolyline frame]: unrecognized selector sent to instance 0x24780a40′
this is how I inserted your code:
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if (!dragging) {
if ([touches count] == 1) {
UITouch *touch = [touches anyObject];
for (MKOverlayView *overlayView in self.myMapView.overlays) { // this is where things can get inefficient if you have loads of overlayViews that aren’t even visible!
CGSize overlayViewSize = overlayView.frame.size;
CGPoint touchPoint = [touch locationInView:overlayView];
if (touchPoint.x > 0 && touchPoint.x 0 && touchPoint.y < overlayViewSize.height) {
[self overlayTouched:overlayView]; // overlayView is the one that was touched!
}
}
}
}
}
The overlays in the mapview are in fact polylines.
How may I fix it?
Thanks, Fabrizio
Hi,
Thank you for this great example.
PS : on your first piece of code, it’s [UIGestureRecognizer alloc]