I have having problems using a UILongPressGestureRecognizer together a draggable MKPinAnnotationView.
The behaviour I am trying to produce is similar to the Maps App.
The pin can be dragged.
When there is a long press/ tap, a pin is dropped.
However, I have problems having the long press being recognized outside the frame of the MKPinAnnotationView. The long press gesture to drop the pin works fine if the Pin is not draggable. When the pin is draggable however, I can't get the long press gesture recognizer to be recognized so that I can drop pin.
Any ideas?
By the way, I have tried to set the delegate for the long press recognizer so that
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
In this case, the long press gestures are recognized and the pins are dropped, but the dragging of the pin no longer works.
Snippets of the MapView (a subclass of MKMapView)
- (id)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
// init the gesture recognizer
UILongPressGestureRecognizer* lpgr = [[UILongPressGestureRecognizer alloc]
initWithTarget:self action:#selector(handleLongPress:)];
lpgr.minimumPressDuration = 0.5f; //user needs to press for 2 seconds
lpgr.delegate = self;
[self addGestureRecognizer:lpgr];
[lpgr release];
//add some initial annotation
Marker *_annotation = [[Marker alloc] initWithCoordinate:_location];
[_annotation titleWithString:#"some title"];
[self addAnnotation:_annotation];
}
return self;
}
- (void)handleLongPress:(UIGestureRecognizer *)gestureRecognizer
{
if (gestureRecognizer.state != UIGestureRecognizerStateBegan)
{
return;
}
CGPoint touchPoint = [gestureRecognizer locationInView:self];
CLLocationCoordinate2D touchMapCoordinate = [self convertPoint:touchPoint toCoordinateFromView:self];
// add marker to self-map
// Marker is subclass of MKAnnotation
Marker *_annotation = [[Marker alloc] initWithCoordinate:_location];
[_annotation titleWithString:#"some title"];
[self addAnnotation:_annotation];
}
- (MKAnnotationView *)mapView:(MKMapView *)mView viewForAnnotation:(id<MKAnnotation>) annotation {
if([annotation isMemberOfClass:[Marker class]] ) {
// use MKPinAnnotationView for the view
MKPinAnnotationView *_pin = (MKPinAnnotationView *) [mView dequeueReusableAnnotationViewWithIdentifier:#"spot_pin"];
if (_pin == nil)
{
_pin = [[[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:#"spot_pin"] autorelease];
}
else
{
_pin.annotation = annotation;
}
[_pin setDraggable:YES];
[_pin setSelected:YES animated:YES];
[_pin setCanShowCallout:YES];
return _pin;
} else {
return nil;
}
}
Ok guys, I solved it.
Apparently when after I subclassed MKMapView, I also added a method handleLongPress. This method apparently interfered with the handleLongPress method of the MKMapView.
Just by changing my handleLongPress selector to a different name like handleLongPress2 will make it work like the Maps app.
UILongPressGestureRecognizer* lpgr = [[UILongPressGestureRecognizer alloc]
initWithTarget:self action:#selector(handleLongPress2:)];
Related
I am loading the mapview as below, with MKAnnotationView added as subview in the mapview. I have added as subview, instead of addAnnotation, as I wanted the annotation view in the center all the time and the mapview to scroll under it.
- (void)viewDidLoad {
[super viewDidLoad];
self.mapView.delegate = self;
[self.mapView addSubview:self.centerAnnotationView];
}
- (MKPointAnnotation *)centerAnnotaion
{
if (!_centerAnnotaion) {
_centerAnnotaion = [[MKPointAnnotation alloc] init];
_centerAnnotaion.title = #"Title";
}
return _centerAnnotaion;
}
- (MKPinAnnotationView *)centerAnnotationView
{
if (!_centerAnnotationView) {
_centerAnnotationView = [[MKPinAnnotationView alloc] initWithAnnotation:self.centerAnnotaion
reuseIdentifier:#"centerAnnotationView"];
_centerAnnotationView.pinColor = MKPinAnnotationColorGreen;
_centerAnnotationView.canShowCallout = YES;
_centerAnnotationView.rightCalloutAccessoryView = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
}
return _centerAnnotationView;
}
I have the following questions.
Is it possible to get the default callout even when I haven't added the pin as annotation to the map view.
Is there a way that I could get - (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view event working in my case.
Should I use the tap gesture to capture the tap on the annotation pin?
Currently, I am having an issue with my project in implementing a custom MKAnnotationView that has multiple custom UIImageViews. So these custom UIImageViews have a clear button on top of them to not have to add gesture recognizers.
As you can see, it would be beneficial to actually tap the MKAnnotationView subviews and have some action happen.
I implemented a protocol for the MKAnnotationView where each image subview within the MKAnnotationView makes a callback to the controller that is the owner of the MKMapView... Heres the code...
PHProfileImageView *image = [[PHProfileImageView alloc] initWithFrame:CGRectMake(newX - radius / 5.0f, newY - radius / 5.0f, width, height)];
[image setFile:[object objectForKey:kPHEventPictureKey]];
[image.layer setCornerRadius:image.frame.size.height/2];
[image.layer setBorderColor:[[UIColor whiteColor] CGColor]];
[image.layer setBorderWidth:2.0f];
[image.layer setMasksToBounds:YES];
[image.profileButton setTag:i];
[image.profileButton addTarget:self action:#selector(didTapEvent:) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:image];
- (void)didTapEvent:(UIButton *)button
{
NSLog(#"%#", [self.pins objectAtIndex:button.tag]);
if (self.delegate && [self.delegate respondsToSelector:#selector(didTapEvent:)]) {
[self.delegate JSClusterAnnotationView:self didTapEvent:[self.pins objectAtIndex:button.tag]];
}
}
So as you can see, I already attempt to log the result of the tapped image but nothing :(. Is the way I'm implementing this not the way to go? Am I supposed to have CAShapeLayers or something? Not really sure at this point. Anyone got any ideas?
Edit
Im thinking that I might have to implement a custom callout view. Since a callout view actually adds buttons to its view and can respond to touch events... Not totally sure though because callouts are only shown once the annotation view is tapped. And in this case, the ACTUAL annotation view is the middle label
So I resized the mkannotationview's frame to a much larger frame and apparently all the subviews are actually not within the MKAnnotationView's bounds, so the subviews aren't actually being tapped. Now that Im thinking about this solution, it probably wasn't the best solution.
If anyone has any suggestions rather than adding subviews to a MKAnnotationView to create the view I currently have, that would be great!
For the Custom AnnotationView with Clickable Buttons, you have to create custom AnnotationView SubClass in the Project. For that create a new file.
And add these two methods to the implementation file.
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
UIView* hitView = [super hitTest:point withEvent:event];
if (hitView != nil)
{
[self.superview bringSubviewToFront:self];
}
return hitView;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event
{
CGRect rect = self.bounds;
BOOL isInside = CGRectContainsPoint(rect, point);
if(!isInside)
{
for (UIView *view in self.subviews)
{
isInside = CGRectContainsPoint(view.frame, point);
if(isInside)
break;
}
}
return isInside;
}
Then go to the ViewController.m file again and modify the viewDidLoad method as this.
- (void)viewDidLoad {
[super viewDidLoad];
self.mapKit.delegate = self;
//Set Default location to zoom
CLLocationCoordinate2D noLocation = CLLocationCoordinate2DMake(51.900708, -2.083160); //Create the CLLocation from user cordinates
MKCoordinateRegion viewRegion = MKCoordinateRegionMakeWithDistance(noLocation, 50000, 50000); //Set zooming level
MKCoordinateRegion adjustedRegion = [self.mapKit regionThatFits:viewRegion]; //add location to map
[self.mapKit setRegion:adjustedRegion animated:YES]; // create animation zooming
// Place Annotation Point
MKPointAnnotation *annotation1 = [[MKPointAnnotation alloc] init]; //Setting Sample location Annotation
[annotation1 setCoordinate:CLLocationCoordinate2DMake(51.900708, -2.083160)]; //Add cordinates
[self.mapKit addAnnotation:annotation1];
}
Now add that custom View to the ViewController.xib.
Now create this delegate method as below.
#pragma mark : MKMapKit Delegate
-(MKAnnotationView *)mapView:(MKMapView *)mV viewForAnnotation:(id <MKAnnotation>)annotation
{
AnnotationView *pinView = nil; //create MKAnnotationView Property
static NSString *defaultPinID = #"com.invasivecode.pin"; //Get the ID to change the pin
pinView = (AnnotationView *)[self.mapKit dequeueReusableAnnotationViewWithIdentifier:defaultPinID]; //Setting custom MKAnnotationView to the ID
if ( pinView == nil )
pinView = [[AnnotationView alloc]
initWithAnnotation:annotation reuseIdentifier:defaultPinID]; // init pinView with ID
[pinView addSubview:self.customView];
addSubview:self.customView.center = CGPointMake(self.customView.bounds.size.width*0.1f, -self.customView.bounds.size.height*0.5f);
pinView.image = [UIImage imageNamed:#"Pin"]; //Set the image to pinView
return pinView;
}
I also got this answer few months ago from someone posted on Stackoverflow. I modified it to my project as I want. Hope this will do your work.
I need to dismiss the presentViewController when click the outside the controller, I'm using the following code
UITapGestureRecognizer *tapBehindGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapBehindDetected:)];
[tapBehindGesture setNumberOfTapsRequired:1];
[tapBehindGesture setCancelsTouchesInView:NO];
[self.view.window addGestureRecognizer:tapBehindGesture];
- (void)tapBehindDetected:(UITapGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateEnded)
{
CGPoint location = [sender locationInView:nil]; //Passing nil gives us coordinates in the window
//Then we convert the tap's location into the local view's coordinate system, and test to see if it's in or outside. If outside, dismiss the view.
if (![self.view pointInside:[self.view convertPoint:location fromView:self.view.window] withEvent:nil])
{
// Remove the recognizer first so it's view.window is valid.
[self.view.window removeGestureRecognizer:sender];
[self dismissViewControllerAnimated:YES completion:nil];
}
}
}
its working fine for me, but i had problem in another view. Inside the view controller I add UITapGestureRecognizer to vwHeaderview.
UITapGestureRecognizer *addNewContactsingleFingerTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleSingleTap:)];
[vwHeaderview addGestureRecognizer:addNewContactsingleFingerTap];
- (void)handleSingleTap:(UIGestureRecognizer *)gestureRecognizer
{
//Some code
}
If I click the vwHeaderview It call the "tapBehindDetected", I wont call "handleSingleTap".
If I comment above "tapBehindGesture" Its working fine. But I need both to work. any help
I add delegate to vwHeaderview and use following function & its working fine for me...
UITapGestureRecognizer *addNewContactsingleFingerTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleSingleTap:)];
addNewContactsingleFingerTap.delegate = self;
[vwHeaderview addGestureRecognizer:addNewContactsingleFingerTap];
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
I have a mapview in my app which needs customized calloutView for every map annotations.
Therefore, I have an XIB file for this customized calloutView.
Here is my code for the map view controller
- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view {
CustomCalloutView *calloutView = (CustomCalloutView *)[[[NSBundle mainBundle] loadNibNamed:#"CustomCalloutView" owner:self options:nil] objectAtIndex:0];
[calloutView.layer setCornerRadius:10];
CGRect calloutViewFrame = calloutView.frame;
calloutViewFrame.origin = CGPointMake(-calloutViewFrame.size.width/2 + 15, -calloutViewFrame.size.height);
calloutView.frame = calloutViewFrame;
// some other code such as load images for calloutView
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleSingleTap)];
singleTap.numberOfTapsRequired = 1;
[calloutView addGestureRecognizer:singleTap];
[view addSubview:calloutView];
}
- (void)handleSingleTap{
NSLog(#"it works");
}
However, the handleSingleTap: has never been called. Instead, every tap on the calloutView will only simply dismiss the calloutView. I also tried to add a button on the calloutView, but tap on it will also cause the calloutView dismissing, rather than calling the button action.
Can anyone help?
Update:
I've tried to change the code
[view addSubview:calloutView];
to
[self.view addSubview:calloutView];
which add the customized calloutView into the main container view rather than the mapView.
Then, it works fine with the tap gesture. Therefore, I think the problem should be caused by the mapView, it seems that mapView passes all the touch event on calloutView to itself. Anyone have ideas regarding this?
OK. I worked out the solution finally. We need to have a customized class for the MKAnnotationView to solve this. Here is the code in the .m file
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
UIView* hitView = [super hitTest:point withEvent:event];
if (hitView != nil)
{
[self.superview bringSubviewToFront:self];
}
return hitView;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event
{
CGRect rect = self.bounds;
BOOL isInside = CGRectContainsPoint(rect, point);
if(!isInside)
{
for (UIView *view in self.subviews)
{
isInside = CGRectContainsPoint(view.frame, point);
if(isInside)
break;
}
}
return isInside;
}
Hopefully this can be helpful for others.
You need to modify your tapgesture implemenation & the method called.Have a look & check whether it work or not.
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleSingleTap)];
singleTap.numberOfTapsRequired = 1;
[calloutView addGestureRecognizer:singleTap];
[view addSubview:calloutView];
- (void)handleSingleTap:(UIGestureRecognizer *)recognizer {
NSLog(#"it works");
}
I want all my view is tappable and not just leftCalloutAccessoryView or rightCalloutAccessoryView, I also want the center to be tappable as well
You can use,
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint touchPoint = [touch locationInView:calloutView];
BOOL isPointInsideView = [calloutView pointInside:touchPoint withEvent:nil];
if (isPointInsideView)
{
// place your action code.
}
}
or you can use,
UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapCalloutAction:)];
tapGestureRecognizer.delegate = self;
tapGestureRecognizer.numberOfTapsRequired = 1;
[calloutView addGestureRecognizer:tapGestureRecognizer];
-(void) tapCalloutAction:(id)sender
{
// do stuff
}
the idea is that you want to make all of the "accessory view" tappable without interfering with the original tap of the actual annotation.. this is how i do it:
first I create and assign a tap gesture to the annotation view after it has been selected like so (I use obj-c runtime object association here.. see this gist):
// set up vars
static NSString *const kExtraTapGestureRecognizer = #"extraGesture";
UIControl *currentAnnotationControl;
- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view {
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]
initWithTarget:self
action:#selector(handleAnnotationViewTap:)];
tap.numberOfTapsRequired = 1;
[tap setInfo:kExtraTapGestureRecognizer];
[view addGestureRecognizer:tap];
}
to handle the tap, I call MKMapViewDelegate Protocol's mapView:annotationView:calloutAccessoryControlTapped:, which basically means that if the view in between the accessory views gets tapped, it's as if one of the accessory views just got tapped:
- (void)handleAnnotationViewTap:(UITapGestureRecognizer *)gestureRecognizer {
MKAnnotationView *annotationView = (MKAnnotationView *)gestureRecognizer.view;
// currentAnnotationControl is just a reference to one of the
// accessory views of the annotation that has just been selected..
// see comment below
[self mapView:self.mapView
annotationView:annotationView
calloutAccessoryControlTapped:currentAnnotationControl];
}
when the annotationView is returned, I save a reference to one of its (left or right) accessoryViews like so:
- (MKAnnotationView *)mapView:(MKMapView *)theMapView viewForAnnotation:(id <MKAnnotation>)annotation
{
if ([annotation isKindOfClass:[Random class]])
{
static NSString *annotationIdentifier = #"annotation";
MKAnnotationView *annotationView =
(MKAnnotationView *) [self.mapView annotationIdentifier];
if (annotationView == nil)
{
annotationView = [[MKAnnotationView alloc]
initWithAnnotation:annotation reuseIdentifier:RiderAnnotationIdentifier];
annotationView.canShowCallout = YES;
UIButton *leftButton = [UIButton buttonWithType:UIButtonTypeCustom];
leftButton.frame = CGRectMake(0, 0,21, 21);
[leftButton setImage:[UIImage imageNamed:#"smallInfo_rider_left.png"] forState:UIControlStateNormal];
[leftButton addTarget:nil action:nil forControlEvents:UIControlEventTouchUpInside];
annotationView.leftCalloutAccessoryView = leftButton;
// this is where i store a reference to one of the accessory views
currentAnnotationControl = leftButton;
return annotationView;
}
else
{
annotationView.annotation = annotation;
}
return annotationView;
keep in mind that we created an extra tap gesture, we must get rid of it as soon as one of the accessory views has been tapped like so:
- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control
{
// we remove the extra tap gesture so that it doesn't interfere
// with normal app flow in the future
for (UIGestureRecognizer *gesture in [view gestureRecognizers]) {
if ([[gesture info] isEqualToString:kExtraTapGestureRecognizer]) {
[gesture removeTarget:nil action:NULL];
}
}
// more stuff