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
Related
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’ve read a lot about MKAnnotation and how you need to implement setCoordinate in your subclass as well as draggable=TRUE in order to make the whole shebang draggable.
My situation is that in my iOS7-only app, my annotation is draggable no matter whether I implement setCoordinate or not…but the problem is that I need to tap it first (which pops out the callout accessory) AND THEN long tap it, and only then will it hover in the air above the map and can be dragged. This is confusing for the user because it’s different to how it is in the standard Maps app. Notice in the Maps app that a long tap on an annotation will make it hover & draggable without a prerequisite tap.
I’ve tried implementing setCoordinate, but this doesn’t make any difference. Other than that my annotation subclass just stores the latitude & longitude, which works fine. I just want it to be draggable straight away on the long tap.
Relevant code for my View Controller which implements MKMapViewDelegate. I can verify this by putting breakpoints in delegate methods.
- (void)viewDidLoad
{
[super viewDidLoad];
[mapView setDelegate:self];
}
-(MKAnnotationView *)mapView:(MKMapView *)mV viewForAnnotation:
(id <MKAnnotation>)annotation {
MKPinAnnotationView *pinView = nil;
if(annotation != mapView.userLocation)
{
static NSString *defaultPinID = #"pointPin";
pinView = (MKPinAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:defaultPinID];
if ( pinView == nil ) {
pinView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:defaultPinID];
}
pinView.pinColor = MKPinAnnotationColorRed;
if ([annotation isKindOfClass:[SimpleMapAnnotation class]]) {
SimpleMapAnnotation *simpleMapAnnotation = (SimpleMapAnnotation*)annotation;
if ([simpleMapAnnotation color]) {
pinView.pinColor = [simpleMapAnnotation color];
}
if (simpleMapAnnotation.moveable) {
pinView.draggable=TRUE;
// delete button to remove an annotation
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[button setImage:[[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"trash" ofType:#"png"]] forState:UIControlStateNormal] ;
button.frame = CGRectMake(0, 0, 23, 23);
pinView.rightCalloutAccessoryView = button;
}
}
pinView.canShowCallout = YES;
pinView.animatesDrop = YES;
}
else {
[mapView.userLocation setTitle:#"I am here"];
}
return pinView;
}
- (void)mapView:(MKMapView *)theMapView annotationView:(MKAnnotationView *)view
calloutAccessoryControlTapped:(UIControl *)control{
if([view.annotation isKindOfClass:[SimpleMapAnnotation class]]){
SimpleMapAnnotation *annotation = (SimpleMapAnnotation*)view.annotation;
//remove the point from the database
//<snip>
[UIView animateWithDuration:0.3 delay:0.0 options:0 animations:(void (^)(void)) ^{
//remove the annotation from the map
view.alpha = 0.0f;
}
completion:^(BOOL finished){
[theMapView removeAnnotation:annotation];
view.alpha=1.0f;
}];
}
}
- (void)mapView:(MKMapView *)mapView
annotationView:(MKAnnotationView *)annotationView
didChangeDragState:(MKAnnotationViewDragState)newState
fromOldState:(MKAnnotationViewDragState)oldState
{
if (newState == MKAnnotationViewDragStateEnding)
{
if ([annotationView.annotation isMemberOfClass:[SimpleMapAnnotation class]]) {
SimpleMapAnnotation *simpleMapAnnotation = (SimpleMapAnnotation*)annotationView.annotation;
simpleMapAnnotation.latitude = [NSNumber numberWithDouble:simpleMapAnnotation.coordinate.latitude];
simpleMapAnnotation.longitude = [NSNumber numberWithDouble:simpleMapAnnotation.coordinate.longitude];
}
CLLocationCoordinate2D droppedAt = annotationView.annotation.coordinate;
NSLog(#"dropped at %f,%f", droppedAt.latitude, droppedAt.longitude);
}
}
To begin the drag of the MKAnnotationView object it should be selected first. It's by design.
If you want to start moving of the annotation view immediately on a long tap you should set the selected property to YES before the touches of that long tap have been delivered to object.
To do this make a successor of MKPinAnnotationView class as following:
// MKImmideateDragPinAnnotationView.h
#interface MKImmideateDragPinAnnotationView : MKPinAnnotationView
#end
// MKImmideateDragPinAnnotationView.m
#implementation MKImmideateDragPinAnnotationView
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[self setSelected:YES];
[super touchesBegan:touches withEvent:event];
}
#end
Then change MKPinAnnotationView class to MKImmideateDragPinAnnotationView class at pinView allocation in your code:
-(MKAnnotationView *)mapView:(MKMapView *)mV viewForAnnotation:(id <MKAnnotation>)annotation {
...
if ( pinView == nil ) {
pinView = [[MKImmideateDragPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:defaultPinID];
}
...
}
Your pin will start to drag immediately on long tap. And will show callout on single tap, as usual.
That trick will work with any MKAnnotationView class in iOS 6.xx - iOS 7.xx.
I ended up using this solution, which not only gets rid of the need for the first tap before long-tap-drag, but also is very smooth feeling for the user when dragging and has a large draggable area. There's a bit of an explanation from Azavea who implemented it, on their blog here.
I'm nearly done (famous last words) on the next version of my app which uses a slightly modified version of the above, namely that it (a) drags as in the original, (b) allows a tappable action on the annotation and (c) specifically ignores a long-tap on the annotation so that when attempting to drag you don't get annoyed by the unwanted tap action. Once I am done, I plan to fork or branch (I'm a bit new to what you do in this situation) the above github project to add that functionality. If I get really lucky I'd also like to be able to fix or at least configure the quick swipe issue they mention in the 2nd last paragraph of that blog I linked to.
I have a detailDisclousure button on the callout of a MKAnnotation. When this button is pressed I need to call a method passing a parameter that identifies the annotation that called it. How could it be done?
this is my viewForAnnotation:
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation
{
if ([annotation isKindOfClass:[Annotation class]])
{
static NSString* identifier = #"identifier";
MKPinAnnotationView* pinView = (MKPinAnnotationView*)[mapView dequeueReusableAnnotationViewWithIdentifier:identifier];
if (pinView == nil)
{
pinView = [[[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:identifier]autorelease];
}
Annotation *antt = annotation;
pinView.canShowCallout = YES;
pinView.draggable = YES;
UIButton* detailButton = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
[detailButton performSelector:#selector(goToViewWithAnnotation:) withObject:antt];
pinView.rightCalloutAccessoryView = detailButton;
return pinView;
}
else
{
return nil;
}
}
And this is the method that should be called with the parameter:
-(void)goToViewWithAnnotation:(Annotation *)annotation
{
NextViewController *nextView = [[NextViewController alloc]initWithNibName:#"NextViewController" bundle:nil];
nextView.id = annotation.id;
[self.navigationController nextView animated:YES];
}
you can pass any NSInteger via the tag property of the UIButton only.
It's not possible to send data with the -addTarget:action:forControlEvent: for UIButton:
[detailButton addTarget:self action:#selector(goToViewWithAnnotation:) forControlEvents:UIControlEventTouchUpInside];
Try to handle the data with some other way, when you trigger the selector. Like saving a pointer or var as you like.
UIButton have these callers as you can see:
- (void)action
- (void)action:(id)sender
- (void)action:(id)sender forEvent:(UIEvent *)event
I think you can add a property(which is (Annotation *)annotation) in .h file. Then you can use annotation in the "-(void)goToViewWithAnnotation" method. Or you can customized a new button which inherits UIControl
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:)];
Here's a sample custom callout that I'd want to get a similar style of. I was thinking of inheriting from MKAnnotation but I'm lost how to start it and I don't know if the callout's design could be overridden.
Any ideas how to implement this custom callout with ui controls inside?
EDIT: Here's my code from following the similar StackOverflow answer:
- (MKAnnotationView *)mapView:(MKMapView *)mapview viewForAnnotation:(id <MKAnnotation>)annotation
{
if ([annotation isKindOfClass:[MKUserLocation class]])
return nil;
static NSString* AnnotationIdentifier = #"AnnotationIdentifier";
MKAnnotationView *annotationView = [mapView dequeueReusableAnnotationViewWithIdentifier:AnnotationIdentifier];
if(annotationView)
return annotationView;
else
{
UIImage *img = [UIImage imageNamed:#"default_thumb.png"];
MKAnnotationView *annotationView =
[[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:AnnotationIdentifier];
annotationView.canShowCallout = YES;
annotationView.image = img;
annotationView.draggable = YES;
/*
// change left accessory view button with image
UIImageView *leftAccView = [[UIImageView alloc] initWithImage:img];
annotationView.leftCalloutAccessoryView = leftAccView;
//include a right button to show more info
UIButton* rightButton = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
[rightButton addTarget:self action:#selector(calloutSubMenu:) forControlEvents:UIControlEventTouchUpInside];
[rightButton setTitle:annotation.title forState:UIControlStateNormal];
annotationView.rightCalloutAccessoryView = rightButton;
*/
return annotationView;
}
return nil;
}
// Customize accessory view
- (void)mapView:(MKMapView *)mapview annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control
{
[mapview deselectAnnotation:view.annotation animated:YES];
PopOverCallout *popOverCallout = [[PopOverCallout alloc]init];
UIPopoverController *popOver = [[UIPopoverController alloc] initWithContentViewController:popOverCallout];
self.annotationPopoverController = popOver;
popOver.popoverContentSize = CGSizeMake(500, 500);
[popOver presentPopoverFromRect:view.bounds inView:view permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
}
The
(void)mapView:(MKMapView *)mapview annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control
should be called whenever the pin is touched to show the callout right? but what happens is that a blank regular callout is shown. What might I be doing wrong?
BTW: the UIViewController subclass only has a label on it's XIB and is pretty much blank.
Found out the answer after a few mins of nap.
using this as the guide, I just needed to override
-(void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view
which shows the annotation callout. Thanks again for the answer, Ms. Anna.