views:

11690

answers:

15

Hello,

I'm not sure what i'm doing wrong but I try to catch touches on a MKMapView object. I subclassed it by creating the following class :

#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>

@interface MapViewWithTouches : MKMapView {

}

- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *) event;   

@end

And the implementation :

#import "MapViewWithTouches.h"
@implementation MapViewWithTouches

- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *) event {

    NSLog(@"hello");
    //[super touchesBegan:touches withEvent:event];

}
@end

But it looks like when I use this class, I see nothing on the Console :

MapViewWithTouches *mapView = [[MapViewWithTouches alloc] initWithFrame:self.view.frame];
[self.view insertSubview:mapView atIndex:0];

Any idea what I'm doing wrong?

Heelp! :)

Thanks a lot!

Martin

+1  A: 

I haven't experimented, but there's a good chance MapKit is based around a class cluster, and therefore subclassing it is difficult and ineffective.

I'd suggest making the MapKit view a subview of a custom view, which should allow you to intercept touch events before they reach it.

grahamparks
Hello Graham!Thank you for your help! If I make a super custom view like you suggest, how could I then forward events to the MKMapView? Any idea?
Martin
+2  A: 

You probably will need to overlay a transparent view to catch the touches just like is done so often with UIWebView-based controls. The Map View already does a bunch of special things with a touch in order to allow the map to be moved, centered, zoomed, etc... that the messages are not getting bubbled up to your app.

Two other (UNTESTED) options I can think of:

1) Resign the first responder via IB and set it to "File's Owner" to allow file's Owner to respond to the touches. I an dubious that this will work because MKMapView extends NSObject, not UIView ans a result the touch events still may not get propagated up to you.

2) If you want to trap when the Map state changes (such as on a zoom) just implement the MKMapViewDelegate protocol to listen for particular events. My hunch is this is your best shot at trapping some interaction easily (short of implementing the transparent View over the Map). Do not forget to set the View Controller housing the MKMapView as the map's delegate (map.delegate = self).

Good Luck.

MystikSpiral
MKMapView definitely subclasses UIView.
Daniel Dickison
A: 

Make the MKMapView a subview of a custom view and implement

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event

in the custom view to return self instead of the subview.

Peter N Lewis
Hello Peter, Thanks for your answer!But I think that by doing that, the MKMapView might not be able to get any touche events, isn't it?I'm looking for a way to just catch event and then forward it to the MKMapView.
Martin
+19  A: 

After a day of pizzas, screamings, I finally found the solution! Very neat!

Peter, I used your trick above and tweaked it a little bit to finally have a solution which work perfectly with MKMapView and should work also with UIWebView

MKTouchAppDelegate.h

#import <UIKit/UIKit.h>
@class UIViewTouch;
@class MKMapView;

@interface MKTouchAppDelegate : NSObject <UIApplicationDelegate> {
    UIWindow *window;
    UIViewTouch *viewTouch;
    MKMapView *mapView;
}
@property (nonatomic, retain) UIViewTouch *viewTouch;
@property (nonatomic, retain) MKMapView *mapView;
@property (nonatomic, retain) IBOutlet UIWindow *window;

@end

MKTouchAppDelegate.m

#import "MKTouchAppDelegate.h"
#import "UIViewTouch.h"
#import <MapKit/MapKit.h>

@implementation MKTouchAppDelegate

@synthesize window;
@synthesize viewTouch;
@synthesize mapView;


- (void)applicationDidFinishLaunching:(UIApplication *)application {

    //We create a view wich will catch Events as they occured and Log them in the Console
    viewTouch = [[UIViewTouch alloc] initWithFrame:CGRectMake(0, 0, 320, 480)];

    //Next we create the MKMapView object, which will be added as a subview of viewTouch
    mapView = [[MKMapView alloc] initWithFrame:CGRectMake(0, 0, 320, 480)];
    [viewTouch addSubview:mapView];

    //And we display everything!
    [window addSubview:viewTouch];
    [window makeKeyAndVisible];


}


- (void)dealloc {
    [window release];
    [super dealloc];
}


@end

UIViewTouch.h

#import <UIKit/UIKit.h>
@class UIView;

@interface UIViewTouch : UIView {
    UIView *viewTouched;
}
@property (nonatomic, retain) UIView * viewTouched;

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

@end

UIViewTouch.m

#import "UIViewTouch.h"
#import <MapKit/MapKit.h>

@implementation UIViewTouch
@synthesize viewTouched;

//The basic idea here is to intercept the view which is sent back as the firstresponder in hitTest.
//We keep it preciously in the property viewTouched and we return our view as the firstresponder.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"Hit Test");
    viewTouched = [super hitTest:point withEvent:event];
    return self;
}

//Then, when an event is fired, we log this one and then send it back to the viewTouched we kept, and voilà!!! :)
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"Touch Began");
    [viewTouched touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"Touch Moved");
    [viewTouched touchesMoved:touches withEvent:event];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"Touch Ended");
    [viewTouched touchesEnded:touches withEvent:event];
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"Touch Cancelled");
}

@end

I hope that will help some of you!

Cheers

Martin
Nice. Small suggestion: You should avoid naming your own classes with a UI prefix. Apple reserves/discourages using NS or UI as a class prefix, because these might end up clashing with an Apple class (even if it's a private class).
Daniel Dickison
Hey Daniel,You are perfectly right, I thought that too!To complete my answer above, let me add a little warning : My Example assume there's only one object viewTouched, which are consuming all the events. But that's not true. You could have some Annotations on top of your Map and then my code doesn't work anymore. To work 100%, you need to remember for each hitTest the view associated to that specific event (and eventually release it when touchesEnded or touchesCancelled is triggered so you don't need to keep track of finished events...).
Martin
Very useful code, thanks Martin! I was wondering if you tried pinch-zooming the map after implementing this? For me when I got it working using basically the same code you have above everything seemed to work except pinch zooming the map. Anyone have any ideas?
Adam Alexander
Hey Adam,I also have this limitation and I don't really understand why! That really is annoying. If you find a solution, let me know! Thx
Martin
Ok, I voted this one up because it initially appeared to solve my problem.HOWEVER...!I can't seem to get multi-touch to work. That is, even though I directly pass touchesBegan and touchesMoved to viewTouched (I do my intercepting on touchesEnded), I cannot zoom the map with pinch gestures.(Continued...)
Olie
(Continued from above.) In fact, this code: In fact, this code:- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { NSLog(@"Map touch-began count: %d", [touches count]); [viewTouched touchesBegan:touches withEvent:event];}- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { NSLog(@"Touch Moved: %d", [touches count]); [viewTouched touchesMoved:touches withEvent:event];}Yields this log output on attempted pinch/zoom: Map touch-began count: 1 Touch Moved: 1 Touch Moved: 1[etc., snip]Help?!
Olie
(Bah! That went poorly. Hopefully someone with edit privileges can fix that.)
Olie
Wow! Nicely done. Next question: Can I detect when a PIN is tapped (and the annotation/callout pops into view)? I don't see a way to do that yet. :(
Joe D'Andrea
Should I be creating a delegate that is set in UIViewTouch to be called in one of the touches method?For example, I am embedding my version of UIViewTouch (which i call MiniMapViewTouch) in a custom UITableViewCell. When the UIViewTouch is clicked, I want it to perform a method in the custom cell. Therefore, I should create a delegate, set it, and then show the UIViewTouch correct?
Kevin Elliott
I ended up creating a delegate chain and it worked well.This answer was spot on. Thank you!
Kevin Elliott
You should release the touchView and mapView in your app delegate's dealloc method.
Colins
Great ! will this work for uiwebview ? i mean for pinching events as well ?
thndrkiss
A: 

Thanks for the pizza and screamings - you saved me lots of time.

multipletouchenabled will work sporadically.

viewTouch.multipleTouchEnabled = TRUE;

In the end, I switched out the views when I needed to capture the touch (different point in time than needing pinchzooms):

 [mapView removeFromSuperview];
 [viewTouch addSubview:mapView];
 [self.view insertSubview:viewTouch atIndex:0];
BankStrong
but does not work with live zooming. It also seems to always zoom out.
Roger Nolan
A: 

Hi Guys,

I've got this to work by observing the "contentOffset" value within the MKScrollView contained within the MKMapView.

Best,

mpancholy

A: 

Hey mpancholy,

Can you describe what you done to make it works?

Thanks

Tony
A: 

I notice that you can track the number and location of touches, and get the location of each in a view:

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"Touch Moved %d", [[event allTouches] count]);

 NSEnumerator *enumerator = [touches objectEnumerator];
 id value;

 while ((value = [enumerator nextObject])) {
  NSLog(@"touch description %f", [value locationInView:mapView].x);
 }
    [viewTouched touchesMoved:touches withEvent:event];
}

Has anyone else tried using these values to update the map's zoom level? It would be a matter of recording the start positions, and then the finish locations, calculating the relative difference and updating the map.

I'm playing with the basic code provided by Martin, and this looks like it will work...

Dan Donaldson
A: 

Here's what I put together, that does allow pinch zooms in the simulator (haven't tried on a real iPhone), but I think would be fine:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"Touch Began %d", [touches count]);
 reportTrackingPoints = NO;
 startTrackingPoints = YES;
    [viewTouched touchesBegan:touches withEvent:event];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
 if ([[event allTouches] count] == 2) {
  reportTrackingPoints = YES;
  if (startTrackingPoints == YES) {
   BOOL setA = NO;
   NSEnumerator *enumerator = [[event allTouches] objectEnumerator];
   id value;
   while ((value = [enumerator nextObject])) {
    if (! setA) {
     startPointA = [value locationInView:mapView];
     setA = YES;
    } else {
     startPointB = [value locationInView:mapView];
    }
   }
   startTrackingPoints = NO;
  } else {
   BOOL setA = NO;
   NSEnumerator *enumerator = [[event allTouches] objectEnumerator];
   id value;
   while ((value = [enumerator nextObject])) {
    if (! setA) {
     endPointA = [value locationInView:mapView];
     setA = YES;
    } else {
     endPointB = [value locationInView:mapView];
    }
   }
  }
 }
 //NSLog(@"Touch Moved %d", [[event allTouches] count]);
    [viewTouched touchesMoved:touches withEvent:event];
}

- (void) updateMapFromTrackingPoints {
 float startLenA = (startPointA.x - startPointB.x);
 float startLenB = (startPointA.y - startPointB.y);
 float len1 = sqrt((startLenA * startLenA) + (startLenB * startLenB));
 float endLenA = (endPointA.x - endPointB.x);
 float endLenB = (endPointA.y - endPointB.y);
 float len2 = sqrt((endLenA * endLenA) + (endLenB * endLenB));
 MKCoordinateRegion region = mapView.region;
 region.span.latitudeDelta = region.span.latitudeDelta * len1/len2;
 region.span.longitudeDelta = region.span.longitudeDelta * len1/len2;
 [mapView setRegion:region animated:YES];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
 if (reportTrackingPoints) {
  [self updateMapFromTrackingPoints];
  reportTrackingPoints = NO;
 }


    [viewTouched touchesEnded:touches withEvent:event];
}

The main idea is that if the user is using two fingers, you track the values. I record the starting and ending points in startPoints A and B. Then I record the current tracking points, and when I'm done, on touchesEnded, I can call a routine to calculate the relative lengths of the line between the points I start with, and the line between the point I end with using simple hypotenuse calc. The ratio between them is the zoom amount: I multiply the region span by that amount.

Hope it's useful to someone.

Dan Donaldson
A: 

Hi.

I followed Dan D's code and everything works except pinch. Then I get a crash at this line:

MKCoordinateRegion region = mapView.region;

It seems there is no region. I probably did something wrong when setting up the project. Do I need to set MKMapViewDelegate somewhere to get this to work?

Thanks Andreas

Andreas Blomqvist
+1  A: 

Hi all,

So after half a day of messing around with this I found the following:

  1. As everyone else found, pinching doesn't work. I tried both subclassing MKMapView and the method described above (intercepting it). And the result is the same.
  2. In the Stanford iPhone videos, a guy from Apple says that many of the UIKit things will cause alot of errors if you "transfer" the touch requests (aka the two methods described above), and you probably won't get it to work.

  3. THE SOLUTION: is described here: http://stackoverflow.com/questions/1121889/intercepting-hijacking-iphone-touch-events-for-mkmapview/1298330. Basically you "catch" the event before any responder gets it, and interpret it there.

A: 

I took the idea of an "overlay" transparent view, from MystikSpiral's answer, and it worked perfectly for what I was trying to achieve; quick, and clean solution.

In short, I had a custom UITableViewCell (designed in IB) with a MKMapView on the left-hand-side and some UILabels on the right. I wanted to make the custom cell so you could touch it anywhere and this would push a new view controller. However touching the map didn't pass touches 'up' to the UITableViewCell until I simply added a UIView of the same size as the map view right on top of it (in IB) and made it's background the 'clear color' in code (don't think you can set clearColor in IB??):

dummyView.backgroundColor = [UIColor clearColor];

Thought it might help someone else; certainly if you want to achieve the same behaviour for a table view cell.

petert
A: 

Folks,

i've tried Martin's code above, but it seens the touch event never passed to subview object (MKMapView)

am i missing anything ?

edc
A: 

Martin That works for me in webview thanks

This should be a comment not an "answer".
bentford
A: 

None of the previous solutions above will work perfectly, especially in cases involving multitouch. This solutions is the least intrusive, and from my experience the best.

The best way I have found to achieve this is with a Gesture Recognizer. Other ways turn out to involve a lot of hackish programming that imperfectly duplicates Apple's code.

Here's what I do: Implement a gesture recognizer that cannot be prevented and that cannot prevent other gesture recognizers. Add it to the map view, and then use the gestureRecognizer's touchesBegan, touchesMoved, etc. to your fancy.

How to detect any tap inside an MKMapView (sans tricks)

WildcardGestureRecognizer * tapInterceptor = [[WildcardGestureRecognizer alloc] init];
tapInterceptor.touchesBeganCallback = ^(NSSet * touches, UIEvent * event) {
        self.lockedOnUserLocation = NO;
};
[mapView addGestureRecognizer:tapInterceptor];

WildcardGestureRecognizer.h

//
//  WildcardGestureRecognizer.h
//  Copyright 2010 Floatopian LLC. All rights reserved.
//

#import <Foundation/Foundation.h>

typedef void (^TouchesEventBlock)(NSSet * touches, UIEvent * event);

@interface WildcardGestureRecognizer : UIGestureRecognizer {
    TouchesEventBlock touchesBeganCallback;
}
@property(copy) TouchesEventBlock touchesBeganCallback;


@end

WildcardGestureRecognizer.m

//
//  WildcardGestureRecognizer.m
//  Created by Raymond Daly on 10/31/10.
//  Copyright 2010 Floatopian LLC. All rights reserved.
//

#import "WildcardGestureRecognizer.h"


@implementation WildcardGestureRecognizer
@synthesize touchesBeganCallback;

-(id) init{
    if (self = [super init])
    {
        self.cancelsTouchesInView = NO;
    }
    return self;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    if (touchesBeganCallback)
        touchesBeganCallback(touches, event);
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
}

- (void)reset
{
}

- (void)ignoreTouch:(UITouch *)touch forEvent:(UIEvent *)event
{
}

- (BOOL)canBePreventedByGestureRecognizer:(UIGestureRecognizer *)preventingGestureRecognizer
{
    return NO;
}

- (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)preventedGestureRecognizer
{
    return NO;
}

@end
gonzojive