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.
Related
Setting up a map view with annotation of nearby cafes.
But when I try to custom the annotations using MKMarkerAnnotationView, nothing shows on the map view.
and when I log the marker, the frame is (0 0; 0 0);
I tried pin too, still didn't work.
I set up delegate on storyboard already.
I also debugged the view controller there are marker views but it is not displaying them because the width and height are zero ?
- (void)viewDidLoad {
[super viewDidLoad];
[self.cafeMap setShowsUserLocation:YES];
[self.locationManager requestWhenInUseAuthorization];
self.cafes = [NSMutableArray array];
[self fetchData];
[self.cafeMap registerClass:[MKAnnotationView class] forAnnotationViewWithReuseIdentifier:#"marker"];
}
-(void)fetchData{
[self.networkManager fetchCafeData:^(NSArray * _Nonnull businesses) {
for (NSDictionary* cafeInfo in businesses) {
Cafe *cafe = [[Cafe alloc]initWithCafeInfo:cafeInfo];
[self.cafes addObject:cafe];
}
for (Cafe *cafe in self.cafes) {
[self.cafeMap addAnnotation:cafe];
}
[self.cafeMap showAnnotations:self.cafes animated:YES];
}];
}
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation {
if ([annotation isKindOfClass:[MKUserLocation class]]) {
return nil;
}
MKMarkerAnnotationView *marker = [[MKMarkerAnnotationView alloc]initWithAnnotation:annotation reuseIdentifier:#"marker"];
marker = (MKMarkerAnnotationView*) [mapView dequeueReusableAnnotationViewWithIdentifier:#"marker" forAnnotation:annotation];
UIButton *button = [UIButton buttonWithType:UIButtonTypeInfoLight];
marker.rightCalloutAccessoryView = button;
[marker setEnabled:YES];
[marker setCanShowCallout:YES];
NSLog(#"MARKER:%#",marker);
return marker;
}
This is the output:
MARKER:<MKAnnotationView: 0x7f9fa3f333b0; frame = (0 0; 0 0); layer = <CALayer: 0x60000253d220>>
Note that your message says that it is a MKAnnotationView. That is because you have registered MKAnnotationView for your identifier rather than MKMarkerAnnotationView. You want:
[self.cafeMap registerClass:[MKMarkerAnnotationView class] forAnnotationViewWithReuseIdentifier:#"marker"];
As an aside, you should be able to simplify viewForAnnotation to:
MKAnnotationView *marker = [mapView dequeueReusableAnnotationViewWithIdentifier:#"marker" forAnnotation:annotation];
Personally, I’d move the configuration of the annotation view into its own subclass:
static NSString * const cafeClusteringIdentifier = #"cafe";
#interface CafeAnnotationView: MKMarkerAnnotationView
#end
#implementation CafeAnnotationView
- (instancetype)initWithAnnotation:(id<MKAnnotation>)annotation reuseIdentifier:(NSString *)reuseIdentifier {
if (self = [super initWithAnnotation:annotation reuseIdentifier:reuseIdentifier]) {
UIButton *button = [UIButton buttonWithType:UIButtonTypeInfoLight];
self.rightCalloutAccessoryView = button;
self.canShowCallout = true;
self.clusteringIdentifier = cafeClusteringIdentifier;
}
return self;
}
- (void)setAnnotation:(id<MKAnnotation>)annotation {
[super setAnnotation:annotation];
self.clusteringIdentifier = cafeClusteringIdentifier;
}
#end
By doing this, I avoid bloating my view controller with code for configuring annotation views.
Note, I’m setting the clusteringIdentifier so that you enjoy that behavior of the MKMarkerAnnotationView.
And then I’d register that class for MKMapViewDefaultAnnotationViewReuseIdentifier:
[self.cafeMap registerClass:[CafeAnnotationView class] forAnnotationViewWithReuseIdentifier:MKMapViewDefaultAnnotationViewReuseIdentifier];
The benefit of using MKMapViewDefaultAnnotationViewReuseIdentifier is that you don’t have to implement viewForAnnotation at all. Delete your implementation of that method entirely. In iOS 11 and later, you only need to implement viewForAnnotation if you need to do something special like having multiple custom reuse identifiers for multiple types of annotations.
Anyway, that yields:
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.
MKPinAnnotationView does not allow you to use a custom image as 'pin' and enable dragging at the same time, because the image will change back to the default pin as soon as you start dragging. Therefore I use an MKAnnotationView instead of an MKPinAnnotationView.
While using MKAnnotationView instead of MKPinAnnotationView does keep your custom image shown as your 'pin' it doesn't support the drag & drop animation that you get with the default pin.
Anyway, my issue is that after I drag my custom MKAnnotationView to a new point on the map and then move the map itself the MKAnnotationView does not move with the map anymore.
-(MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation {
static NSString *defaultID = #"myLocation";
if([self.annotation isKindOfClass:[PinAnnotation class]])
{
//Try to get an unused annotation, similar to uitableviewcells
MKAnnotationView *annotationView = [self.mapView dequeueReusableAnnotationViewWithIdentifier:defaultID];
//If one isn't available, create a new one
if(!annotationView)
{
annotationView = [[MKAnnotationView alloc] initWithAnnotation:self.annotation reuseIdentifier:defaultID];
annotationView.canShowCallout = YES;
annotationView.draggable = YES;
annotationView.enabled = YES;
}
UIImageView *imgView = [[UIImageView alloc] initWithFrame:CGRectMake(0.0, 0.0, 32, 32)];
imgView.image = self.passableTag.image;
annotationView.leftCalloutAccessoryView = imgView;
annotationView.image = [UIImage imageNamed:[Constants tagIconImageNameForTagType:self.passableTag.type]];
return annotationView;
}
return nil;
}
Just set the annotationView.dragState to MKAnnotationViewDragStateNone after ending the drag:
- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view didChangeDragState:(MKAnnotationViewDragState)newState fromOldState:(MKAnnotationViewDragState)oldState {
if ([view.annotation isKindOfClass:[PinAnnotation class]]) {
if (newState == MKAnnotationViewDragStateEnding) {
view.dragState = MKAnnotationViewDragStateNone;
}
}
}
I had the same problem and I solved it adding "setDragState" to my MKAnnotationView class.
This is an old solution but it worked to me (iOS8).
Don't reuse MKAnnotationView and it will work.
I'm developing app, where user is localized by gps and then he is asked, whether he is located in specific place. To confirm this, callout bubble is presented to him straight away, asking him, if he is at specific place.
As there is alot of similar questions, I was able to do custom callout bubble:
My problem: button is not "clickable"
My guess: because this custom callout is higher than standard callout bubble, I had to place it in negative "frame", therefore button cannot be clicked. Here is my didSelectAnnotationView method
- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view {
if(![view.annotation isKindOfClass:[MKUserLocation class]]) {
CalloutView *calloutView = (CalloutView *)[[[NSBundle mainBundle] loadNibNamed:#"callOutView" owner:self options:nil] objectAtIndex:0];
CGRect calloutViewFrame = calloutView.frame;
calloutViewFrame.origin = CGPointMake(-calloutViewFrame.size.width/2 + 15, -calloutViewFrame.size.height);
calloutView.frame = calloutViewFrame;
[calloutView.calloutLabel setText:[(MyLocation*)[view annotation] title]];
[calloutView.btnYes addTarget:self
action:#selector(checkin)
forControlEvents:UIControlEventTouchUpInside];
calloutView.userInteractionEnabled = YES;
view.userInteractionEnabled = YES;
[view addSubview:calloutView];
}
}
CalloutView is just simple class with 2 properties(label that shows name of place and button) and with xib.
I have been doing this custom callout bubble for a few days. I tried using "asynchrony solutions" solution but I was unable to add any other kind of button then disclosure button.
My next attempt, was to find something that was easier than asynchrony solutions and modify it to my use. Thats how I found tochi's custom callout.
Based on his work, I was able to customize his bubble and change info button for my custom button. My problem however remained the same. In order to place my custom callout view on top of the pin, I had to give it negative frame, so my button was "clickable" only in bottom 5 pixels. It seems, that I have to maybe dig deeper into ios default callout bubble, subclass it and change frame of callout in there. But I'm really hopeless now.
If you guys could show me the right way, or give me advice, I'll be glad.
There are several approaches to customizing callouts:
The easiest approach is to use the existing right and left callout accessories, and put your button in one of those. For example:
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation {
static NSString *identifier = #"MyAnnotationView";
if ([annotation isKindOfClass:[MKUserLocation class]]) {
return nil;
}
MKPinAnnotationView *view = (id)[mapView dequeueReusableAnnotationViewWithIdentifier:identifier];
if (view) {
view.annotation = annotation;
} else {
view = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:identifier];
view.canShowCallout = true;
view.animatesDrop = true;
view.rightCalloutAccessoryView = [self yesButton];
}
return view;
}
- (UIButton *)yesButton {
UIImage *image = [self yesButtonImage];
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.frame = CGRectMake(0, 0, image.size.width, image.size.height); // don't use auto layout
[button setImage:image forState:UIControlStateNormal];
[button addTarget:self action:#selector(didTapButton:) forControlEvents:UIControlEventPrimaryActionTriggered];
return button;
}
- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control {
NSLog(#"%s", __PRETTY_FUNCTION__);
}
That yields:
If you really don't like the button on the right, where accessories generally go, you can turn off that accessory, and iOS 9 offers the opportunity to specify the detailCalloutAccessoryView, which replaces the callout's subtitle with whatever view you want:
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation {
static NSString *identifier = #"MyAnnotationView";
if ([annotation isKindOfClass:[MKUserLocation class]]) {
return nil;
}
MKPinAnnotationView *view = (id)[mapView dequeueReusableAnnotationViewWithIdentifier:identifier];
if (view) {
view.annotation = annotation;
} else {
view = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:identifier];
view.canShowCallout = true;
view.animatesDrop = true;
}
view.detailCalloutAccessoryView = [self detailViewForAnnotation:annotation];
return view;
}
- (UIView *)detailViewForAnnotation:(PlacemarkAnnotation *)annotation {
UIView *view = [[UIView alloc] init];
view.translatesAutoresizingMaskIntoConstraints = false;
UILabel *label = [[UILabel alloc] init];
label.text = annotation.placemark.name;
label.font = [UIFont systemFontOfSize:20];
label.translatesAutoresizingMaskIntoConstraints = false;
label.numberOfLines = 0;
[view addSubview:label];
UIButton *button = [self yesButton];
[view addSubview:button];
NSDictionary *views = NSDictionaryOfVariableBindings(label, button);
[view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[label]|" options:0 metrics:nil views:views]];
[view addConstraint:[NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeCenterX multiplier:1 constant:0]];
[view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[label]-[button]|" options:0 metrics:nil views:views]];
return view;
}
- (UIButton *)yesButton {
UIImage *image = [self yesButtonImage];
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.translatesAutoresizingMaskIntoConstraints = false; // use auto layout in this case
[button setImage:image forState:UIControlStateNormal];
[button addTarget:self action:#selector(didTapButton:) forControlEvents:UIControlEventPrimaryActionTriggered];
return button;
}
This yields:
If you really want to develop a custom callout yourself, the Location and Maps Programming Guide outlines the steps involved:
In an iOS app, it’s good practice to use the mapView:annotationView:calloutAccessoryControlTapped: delegate method to respond when users tap a callout view’s control (as long as the control is a descendant of UIControl). In your implementation of this method you can discover the identity of the callout view’s annotation view so that you know which annotation the user tapped. In a Mac app, the callout view’s view controller can implement an action method that responds when a user clicks the control in a callout view.
When you use a custom view instead of a standard callout, you need to do extra work to make sure your callout shows and hides appropriately when users interact with it. The steps below outline the process for creating a custom callout that contains a button:
Design an NSView or UIView subclass that represents the custom callout. It’s likely that the subclass needs to implement the drawRect: method to draw your custom content.
Create a view controller that initializes the callout view and performs the action related to the button.
In the annotation view, implement hitTest: to respond to hits that are outside the annotation view’s bounds but inside the callout view’s bounds, as shown in Listing 6-7.
In the annotation view, implement setSelected:animated: to add your callout view as a subview of the annotation view when the user clicks or taps it.
If the callout view is already visible when the user selects it, the setSelected: method should remove the callout subview from the annotation view (see Listing 6-8).
In the annotation view’s initWithAnnotation: method, set the canShowCallout property to NO to prevent the map from displaying the standard callout when the user selects the annotation.
While it's in Swift, https://github.com/robertmryan/CustomMapViewAnnotationCalloutSwift illustrates an example of how you can do this complete customization of the callout (e.g. change shape of callout bubble, change background color, etc.).
That previous point outlines a pretty complicated scenarios (i.e. you have to write your own code to detecting taps outside the view in order to dismiss the it). If you're supporting iOS 9, you might just use a popover view controller, e.g.:
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation {
static NSString *identifier = #"MyAnnotationView";
if ([annotation isKindOfClass:[MKUserLocation class]]) {
return nil;
}
MKPinAnnotationView *view = (id)[mapView dequeueReusableAnnotationViewWithIdentifier:identifier];
if (view) {
view.annotation = annotation;
} else {
view = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:identifier];
view.canShowCallout = false; // note, we're not going to use the system callout
view.animatesDrop = true;
}
return view;
}
- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view {
PopoverController *controller = [self.storyboard instantiateViewControllerWithIdentifier:#"AnnotationPopover"];
controller.modalPresentationStyle = UIModalPresentationPopover;
controller.popoverPresentationController.sourceView = view;
// adjust sourceRect so it's centered over the annotation
CGRect sourceRect = CGRectZero;
sourceRect.origin.x += [mapView convertCoordinate:view.annotation.coordinate toPointToView:mapView].x - view.frame.origin.x;
sourceRect.size.height = view.frame.size.height;
controller.popoverPresentationController.sourceRect = sourceRect;
controller.annotation = view.annotation;
[self presentViewController:controller animated:TRUE completion:nil];
[mapView deselectAnnotation:view.annotation animated:true]; // deselect the annotation so that when we dismiss the popover, the annotation won't still be selected
}
I wound up taking a different approach. I tried the others but they seemed bloated and I didn't want to add more classes or rely on the MKMapViewDelegate to handle the interaction.
I instead override setSelected:animated of my MKAnnotationView subclass. The trick is to expand the bounds of the annotationView after it it selected to fully encompass the call out view, and then return them back to normal after it is deselected. This will allow your custom call outs to accept touches and interactions outside the original bounds of the MKAnnotationView.
Here is a trimmed down code sample to get anyone started:
#define kAnnotationCalloutBoxTag 787801
#define kAnnotationCalloutArrowTag 787802
#define kAnnotationTempImageViewTag 787803
-(void)setSelected:(BOOL)selected animated:(BOOL)animated
{
if (selected == self.selected)
{
NSLog(#"annotation already selected, abort!");
return;
}
if (selected)
{
self.image = nil; //hide default image otherwise it takes the shape of the entire bounds
UIView* calloutBox = [self newCustomCallout];
float imgW = [self unselectedSize].width;
float imgH = [self unselectedSize].height;
float arrowW = 20;
float arrowH = 12;
//Annotation frames wrap a center coordinate, in this instance we want the call out box to fall under the
//central coordinate, so we need to adjust the height to be double what the callout box height would be
//making the height *2, this is to make sure the callout view is inside of it.
self.bounds = CGRectMake(0, 0, calloutBox.frame.size.width, calloutBox.frame.size.height*2 + arrowH*2 + imgH);
CGPoint center = CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2);
UIView* imgView = [[UIImageView alloc] initWithImage:icon];
[imgView setFrame:CGRectMake(center.x - imgW/2, center.y-imgH/2, imgW, imgH)];
imgView.tag = kAnnotationTempImageViewTag;
[self addSubview:imgView];
UIView* triangle = [self newTriangleViewWithFrame:CGRectMake(center.x-arrowW/2, center.y+imgH/2, arrowW, arrowH)];
triangle.tag = kAnnotationCalloutArrowTag;
[self addSubview:triangle];
[calloutBox setFrame:CGRectMake(0, center.y+imgH/2+arrowH, calloutBox.width, calloutBox.height)];
calloutBox.tag = kAnnotationCalloutBoxTag;
[self addSubview:calloutBox];
}
else
{
//return things back to normal
UIView* v = [self viewWithTag:kAnnotationCalloutBoxTag];
[v removeFromSuperview];
v = [self viewWithTag:kAnnotationCalloutArrowTag];
[v removeFromSuperview];
v = [self viewWithTag:kAnnotationTempImageViewTag];
[v removeFromSuperview];
self.image = icon;
self.bounds = CGRectMake(0, 0, [self unselectedSize].width, [self unselectedSize].height);
}
[super setSelected:selected animated:animated];
}
-(CGSize)unselectedSize
{
return CGSizeMake(20,20);
}
-(UIView*)newCustomCallout
{
//create your own custom call out view
UIView* v = [[UIView alloc] initWithFrame:CGRectMake(0,0,250,250)];
v.backgroundColor = [UIColor greenColor];
return v;
}
-(UIView*)newTriangleWithFrame:(CGRect)frame
{
//create your own triangle
UIImageView* v = [[UIImageView alloc] initWithFrame:frame];
[v setImage:[UIImage imageNamed:#"trianglePointedUp.png"]];
return v;
}
(void)mapView:(MKMapView *)mapViewIn didSelectAnnotationView:(MKAnnotationView *)view {
if(![view.annotation isKindOfClass:[MKUserLocation class]])
{
CustomeCalloutViewController *calloutView = [[CustomeCalloutViewController alloc]initWithNibName:#"CustomeCalloutViewController" bundle:nil];
[calloutView setPopinTransitionStyle:BKTPopinTransitionStyleSlide];
[calloutView setPopinTransitionDirection:BKTPopinTransitionDirectionTop];
[self presentPopinController:calloutView animated:YES completion:^{
NSLog(#"Popin presented !");
}];
[mapView deselectAnnotation:view.annotation animated:true];
}
}
I'm currently working with the mapkit and am stuck.
I have a custom annotation view I am using, and I want to use the image property to display the point on the map with my own icon. I have this working fine. But what I would also like to do is to override the default callout view (the bubble that shows up with the title/subtitle when the annotation icon is touched). I want to be able to control the callout itself: the mapkit only provides access to the left and right ancillary callout views, but no way to provide a custom view for the callout bubble, or to give it zero size, or anything else.
My idea was to override selectAnnotation/deselectAnnotation in my MKMapViewDelegate, and then draw my own custom view by making a call to my custom annotation view. This works, but only when canShowCallout is set to YES in my custom annotation view class. These methods are NOT called if I have this set to NO (which is what I want, so that the default callout bubble is not drawn). So I have no way of knowing if the user touched on my point on the map (selected it) or touched a point that is not part of my annotation views (delected it) without having the default callout bubble view show up.
I tried going down a different path and just handling all touch events myself in the map, and I can't seem to get this working. I read other posts related to catching touch events in the map view, but they aren't exactly what I want. Is there a way to dig into the map view to remove the callout bubble before drawing? I'm at a loss.
Any suggestions? Am I missing something obvious?
There is an even easier solution.
Create a custom UIView (for your callout).
Then create a subclass of MKAnnotationView and override setSelected as follows:
- (void)setSelected:(BOOL)selected animated:(BOOL)animated
{
[super setSelected:selected animated:animated];
if(selected)
{
//Add your custom view to self...
}
else
{
//Remove your custom view...
}
}
Boom, job done.
detailCalloutAccessoryView
In the olden days this was a pain, but Apple has solved it, just check the docs on MKAnnotationView
view = MKPinAnnotationView(annotation: annotation, reuseIdentifier: identifier)
view.canShowCallout = true
view.detailCalloutAccessoryView = UIImageView(image: UIImage(named: "zebra"))
Really, that's it. Takes any UIView.
Continuing on from #TappCandy's brilliantly simple answer, if you want to animate your bubble in the same way as the default one, I've produced this animation method:
- (void)animateIn
{
float myBubbleWidth = 247;
float myBubbleHeight = 59;
calloutView.frame = CGRectMake(-myBubbleWidth*0.005+8, -myBubbleHeight*0.01-2, myBubbleWidth*0.01, myBubbleHeight*0.01);
[self addSubview:calloutView];
[UIView animateWithDuration:0.12 delay:0.0 options:UIViewAnimationOptionCurveEaseOut animations:^(void) {
calloutView.frame = CGRectMake(-myBubbleWidth*0.55+8, -myBubbleHeight*1.1-2, myBubbleWidth*1.1, myBubbleHeight*1.1);
} completion:^(BOOL finished) {
[UIView animateWithDuration:0.1 animations:^(void) {
calloutView.frame = CGRectMake(-myBubbleWidth*0.475+8, -myBubbleHeight*0.95-2, myBubbleWidth*0.95, myBubbleHeight*0.95);
} completion:^(BOOL finished) {
[UIView animateWithDuration:0.075 animations:^(void) {
calloutView.frame = CGRectMake(-round(myBubbleWidth/2-8), -myBubbleHeight-2, myBubbleWidth, myBubbleHeight);
}];
}];
}];
}
It looks fairly complicated, but as long as the point of your callout bubble is designed to be centre-bottom, you should just be able to replace myBubbleWidth and myBubbleHeight with your own size for it to work. And remember to make sure your subviews have their autoResizeMask property set to 63 (i.e. "all") so that they scale correctly in the animation.
:-Joe
Found this to be the best solution for me.
You'll have to use some creativity to do your own customizations
In your MKAnnotationView subclass, you can use
- (void)didAddSubview:(UIView *)subview{
int image = 0;
int labelcount = 0;
if ([[[subview class] description] isEqualToString:#"UICalloutView"]) {
for (UIView *subsubView in subview.subviews) {
if ([subsubView class] == [UIImageView class]) {
UIImageView *imageView = ((UIImageView *)subsubView);
switch (image) {
case 0:
[imageView setImage:[UIImage imageNamed:#"map_left"]];
break;
case 1:
[imageView setImage:[UIImage imageNamed:#"map_right"]];
break;
case 3:
[imageView setImage:[UIImage imageNamed:#"map_arrow"]];
break;
default:
[imageView setImage:[UIImage imageNamed:#"map_mid"]];
break;
}
image++;
}else if ([subsubView class] == [UILabel class]) {
UILabel *labelView = ((UILabel *)subsubView);
switch (labelcount) {
case 0:
labelView.textColor = [UIColor blackColor];
break;
case 1:
labelView.textColor = [UIColor lightGrayColor];
break;
default:
break;
}
labelView.shadowOffset = CGSizeMake(0, 0);
[labelView sizeToFit];
labelcount++;
}
}
}
}
And if the subview is a UICalloutView, then you can screw around with it, and what's inside it.
I had the same problem. There is a serious of blog posts about this topic on this blog http://spitzkoff.com/craig/?p=81.
Just using the MKMapViewDelegate doesn't help you here and subclassing MKMapView and trying to extend the existing functionality also didn't work for me.
What I ended up doing is to create my own CustomCalloutView that I am having on top of my MKMapView. You can style this view in any way you want.
My CustomCalloutView has a method similar to this one:
- (void) openForAnnotation: (id)anAnnotation
{
self.annotation = anAnnotation;
// remove from view
[self removeFromSuperview];
titleLabel.text = self.annotation.title;
[self updateSubviews];
[self updateSpeechBubble];
[self.mapView addSubview: self];
}
It takes an MKAnnotation object and sets its own title, afterward it calls two other methods which are quite ugly which adjust the width and size of the callout contents and afterward draw the speech bubble around it at the correct position.
Finally the view is added as a subview to the mapView. The problem with this solution is that it is hard to keep the callout at the correct position when the map view is scrolled. I am just hiding the callout in the map views delegate method on a region change to solve this problem.
It took some time to solve all those problems, but now the callout almost behaves like the official one, but I have it in my own style.
Basically to solve this, one needs to:
a) Prevent the default callout bubble from coming up.
b) Figure out which annotation was clicked.
I was able to achieve these by:
a) setting canShowCallout to NO
b) subclassing, MKPinAnnotationView and overriding the touchesBegan and touchesEnd methods.
Note: You need to handle the touch events for the MKAnnotationView and not MKMapView
I just come up with an approach, the idea here is
// Detect the touch point of the AnnotationView ( i mean the red or green pin )
// Based on that draw a UIView and add it to subview.
- (void)mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated
{
CGPoint newPoint = [self.mapView convertCoordinate:selectedCoordinate toPointToView:self.view];
// NSLog(#"regionWillChangeAnimated newPoint %f,%f",newPoint.x,newPoint.y);
[testview setCenter:CGPointMake(newPoint.x+5,newPoint.y-((testview.frame.size.height/2)+35))];
[testview setHidden:YES];
}
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated
{
CGPoint newPoint = [self.mapView convertCoordinate:selectedCoordinate toPointToView:self.view];
// NSLog(#"regionDidChangeAnimated newPoint %f,%f",newPoint.x,newPoint.y);
[testview setCenter:CGPointMake(newPoint.x,newPoint.y-((testview.frame.size.height/2)+35))];
[testview setHidden:NO];
}
- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view
{
NSLog(#"Select");
showCallout = YES;
CGPoint point = [self.mapView convertPoint:view.frame.origin fromView:view.superview];
[testview setHidden:NO];
[testview setCenter:CGPointMake(point.x+5,point.y-(testview.frame.size.height/2))];
selectedCoordinate = view.annotation.coordinate;
[self animateIn];
}
- (void)mapView:(MKMapView *)mapView didDeselectAnnotationView:(MKAnnotationView *)view
{
NSLog(#"deSelect");
if(!showCallout)
{
[testview setHidden:YES];
}
}
Here - testview is a UIView of size 320x100 - showCallout is BOOL - [self animateIn]; is the function that does view animation like UIAlertView.
You can use leftCalloutView, setting annotation.text to #" "
Please find below the example code:
pinView = (MKPinAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:defaultPinID];
if(pinView == nil){
pinView = [[[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:defaultPinID] autorelease];
}
CGSize sizeText = [annotation.title sizeWithFont:[UIFont fontWithName:#"HelveticaNeue" size:12] constrainedToSize:CGSizeMake(150, CGRectGetHeight(pinView.frame)) lineBreakMode:UILineBreakModeTailTruncation];
pinView.canShowCallout = YES;
UILabel *lblTitolo = [[UILabel alloc] initWithFrame:CGRectMake(2,2,150,sizeText.height)];
lblTitolo.text = [NSString stringWithString:ann.title];
lblTitolo.font = [UIFont fontWithName:#"HelveticaNeue" size:12];
lblTitolo.lineBreakMode = UILineBreakModeTailTruncation;
lblTitolo.numberOfLines = 0;
pinView.leftCalloutAccessoryView = lblTitolo;
[lblTitolo release];
annotation.title = #" ";
I've pushed out my fork of the excellent SMCalloutView that solves the issue with providing a custom view for callouts and allowing flexible widths/heights pretty painlessly. Still some quirks to work out, but it's pretty functional so far:
https://github.com/u10int/calloutview