In my Map application, instead of showing a pin, I want to show a colored background circle with image in it. The color of the background (which is shade of green in below image) circle is dynamic. It will look as in below image:
I created TCircleView which draws the color in "drawRect"
To show similar annotation, I created object of TCircleView and UIImageView and add them to MKAnnotationView object. Its looking good and visible as expected.
But its not allowing to detect tap/touch to show the call out.
I'm using the below code:
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation {
if ([annotation isKindOfClass:[MKPointAnnotation class]]) {
return nil;
}
static NSString *annotationIdentifier = #"StickerPin";
MKAnnotationView *annotationView = (MKAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:annotationIdentifier];
if (!annotationView) {
annotationView = [[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:annotationIdentifier];
annotationView.canShowCallout = YES;
}
TCircleView* circleView = [[TCircleView alloc] init];
circleView.green = [postObj[#"severity"] floatValue]; //dynamic value coming from server
UIImageView* imgView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"Piano"]];
CGRect r = imgView.frame;
r.size.height = r.size.width = 60;
imgView.frame = r;
circleView.frame = r;
[annotationView addSubview:circleView];
[annotationView addSubview:imgView];
return annotationView;
}
Its not allowing to show the callout or not even calling the delegate "didSelectAnnotationView:"
How to show the custom view as annotation on the map?
The default frame width and height for MKAnnotationView is 0,0.
This is most likely preventing it from responding to touches.
Normally, if you set its image property, the frame is automatically set for you.
Since you're not setting the image and adding subviews instead, try manually setting frame to be at least as big as its largest subview.
For example:
imgView.frame = r;
circleView.frame = r;
annotationView.frame = r; // <-- add this line
I created a sub class of annotation view and achieved it. The code is below:
#interface TStickerAnnotationView : MKAnnotationView
#property(nonatomic) float stickerColor;
#end
#interface TStickerAnnotationView () {
UIImageView *_imageView;
TCircleView *_circleView;
}
#end
#implementation TStickerAnnotationView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// make sure the x and y of the CGRect are half it's
// width and height, so the callout shows when user clicks
// in the middle of the image
CGRect viewRect = CGRectMake(-30, -30, 60, 60);
TCircleView* circleView = [[TCircleView alloc] initWithFrame:viewRect];
_circleView = circleView;
[self addSubview:circleView];
UIImageView* imageView = [[UIImageView alloc] initWithFrame:viewRect];
// keeps the image dimensions correct
// so if you have a rectangle image, it will show up as a rectangle,
// instead of being resized into a square
imageView.contentMode = UIViewContentModeScaleAspectFit;
_imageView = imageView;
[self addSubview:imageView];
}
return self;
}
- (void)setImage:(UIImage *)image
{
// when an image is set for the annotation view,
// it actually adds the image to the image view
_imageView.image = image;
}
- (void)stickerColor:(float)color {
_circleView.green = color;
[_circleView setNeedsDisplay];
}
Try looking at this solution- I found it relevant
http://blog.jaanussiim.com/2014/01/28/floating-annotations.html
Related
i have a map view in my view controller, when i make a pin on the desired place i give my own image despite of the default image , but when i run and check the map it always shows default pin in my map instead of my image that i have passed. My code is this,
-(MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation{
MKAnnotationView *view=[self.mapView dequeueReusableAnnotationViewWithIdentifier:#"annoView"];
if (!view) {
view=[[MKAnnotationView alloc]initWithAnnotation:annotation reuseIdentifier:#"annoView"];
if ([annotation isKindOfClass:[MKUserLocation class]]) {
return nil;
}else{
view.image=[UIImage imageNamed:#"car_marker.png"];
view.canShowCallout=YES;
}
}
return view;
}
MapPin *pin=[[MapPin alloc]init];
pin.title=loc[#"name"];
pin.subtitle=nil;
pin.coordinate=annoCordinate;
[self.mapView addAnnotation:pin];
[self.mapView setCenterCoordinate:pin.coordinate animated:YES];
This is very easy to accomplish, you just need to subclass MKAnnotationView and add a custom image view to it. A good example can be found here.
In case the link dies, I have also included the code snippet below, please do show the OP some love though!
#interface TStickerAnnotationView : MKAnnotationView
#property(nonatomic) float stickerColor;
#end
#interface TStickerAnnotationView () {
UIImageView *_imageView;
TCircleView *_circleView;
}
#end
#implementation TStickerAnnotationView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// make sure the x and y of the CGRect are half it's
// width and height, so the callout shows when user clicks
// in the middle of the image
CGRect viewRect = CGRectMake(-30, -30, 60, 60);
TCircleView* circleView = [[TCircleView alloc] initWithFrame:viewRect];
_circleView = circleView;
[self addSubview:circleView];
UIImageView* imageView = [[UIImageView alloc] initWithFrame:viewRect];
// keeps the image dimensions correct
// so if you have a rectangle image, it will show up as a rectangle,
// instead of being resized into a square
imageView.contentMode = UIViewContentModeScaleAspectFit;
_imageView = imageView;
[self addSubview:imageView];
}
return self;
}
- (void)setImage:(UIImage *)image
{
// when an image is set for the annotation view,
// it actually adds the image to the image view
_imageView.image = image;
}
- (void)stickerColor:(float)color {
_circleView.green = color;
[_circleView setNeedsDisplay];
}
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.
So I am trying to replicate the following scenario (translucent annotation views) :
And I have tried unsuccessfully the following implementations:
1- Creating a custom image with 30% opacity and adding to the map ---> Result: The image stays opaque.
Code:
-(id)initWithAnnotation:(id<MKAnnotation>)annotation reuseIdentifier:(NSString *)reuseIdentifier{
self = [super initWithAnnotation:annotation reuseIdentifier:reuseIdentifier];
if (self) {
LLAnnotation *myA = (LLAnnotation*) annotation;
self.accessibilityLabel = myA.title;
self.annotation = myA;
self.enabled = YES;
self.canShowCallout = YES;
self.centerOffset = CGPointMake(5,-10);
self.backgroundColor = [UIColor yellowColor];
self.image = [UIImage imageNamed:#"circle"];
}
return self;
}`
And then adding it in - (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id )annotation_
2- Adding a sublayer to the AnnotationView and clearing it ---> Result: Doesn't show any annotation.
Code:
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation_
{
if (annotation_ == mapView.userLocation) return nil;
MKAnnotationView *m = [[MKAnnotationView alloc] initWithAnnotation:annotation_ reuseIdentifier:#"default"];
// m.backgroundColor = [UIColor clearColor];
CALayer *layer = [[CALayer alloc]init];
layer.frame = m.frame;
layer.backgroundColor = [UIColor lightGreenColor].CGColor;
[m.layer addSublayer:layer];
m.layer.cornerRadius = m.frame.size.width/2;
m.layer.borderWidth = 2;
m.layer.masksToBounds = YES;
return m;
}
I was thinking that adding MKOverlays on top of annotations maybe a workaround but it shouldn't be the way to go I believe.
Does anyone have other suggestions on how to implement this?
Create UIImageView object and make it looks like the image you required.
Add as subview of annotationView in viewForAnnotation delegate method will do the trick.
Also you need to set center position offset for annotation image to render annotation exactly correct position of location.
Have look on below code:
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation_
{
if (annotation_ == mapView.userLocation) return nil;
MKAnnotationView *m = [[MKAnnotationView alloc] initWithAnnotation:annotation_ reuseIdentifier:#"default"];
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(m.center.x, m.center.y, 20, 20)];
[imageView setBackgroundColor:[UIColor colorWithRed:0.7294 green:0.7843 blue:0.1921 alpha:1.0]];
imageView.layer.cornerRadius = imageView.frame.size.width / 2;
imageView.alpha = 0.6f;
[m addSubview:imageView];
// Also set center offset for annotation
[m setCenterOffset:CGPointMake(-10, -20)];
return m;
}
What I would do is create an image in photoshop which has a transparent background, and then add your desired yellow circle on top. Then make the opacity of that circle to what ever opacity you want. Save the image as a PNG.
Once you have saved the image, add it to your Xcode project. Once you've added it, add the following line under your viewForAnnotation.
annotationView.image = [UIImage imageNamed:#"ThisIsThePNGImagesName.png"];
Hope that helps :)
It works great on iOS 7 but UIImageView's position shifts on iO8 like images below. Also rightCalloutAccessoryView position shifts top-right corner.
Can anybody help?
Cheers.
In iOS 7
In iOS 8
-(void)mapView:(MKMapView*)mapView didSelectAnnotationView:(MKAnnotationView*)view
{
if(![view.annotation isKindOfClass:[MKUserLocation class]]) {
UBAnnotation *selectedAnnotation = view.annotation;
NSURL * imageUrlAtIndex = [NSURL URLWithString:[[self.objects objectAtIndex:selectedAnnotation.idx] ad_thumbImageUrl]];
UIImageView * leftCalloutView = [[UIImageView alloc] initWithFrame:CGRectMake(2, 2, 40, 40)];
[leftCalloutView setImageWithURL:imageUrlAtIndex];
leftCalloutView.layer.masksToBounds = YES;
leftCalloutView.layer.cornerRadius = 6;
view.leftCalloutAccessoryView = leftCalloutView;
}
}
-(MKAnnotationView *) mapView:(MKMapView *)mapView viewForAnnotation:(id ) annotation {
MKPinAnnotationView *annView=[[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:#"currentloc"];
if ([annotation isKindOfClass:[MKUserLocation class]]) {
return nil;
} else {
annView.image = [UIImage imageNamed:#"pin"];
annView.rightCalloutAccessoryView = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
annView.rightCalloutAccessoryView.tintColor = [UBColor ubGreenColor];
annView.animatesDrop= NO;
annView.canShowCallout = YES;
}
return annView;
}
I solved it!
It's all about titleLabel's string length. If the string length over 20-25 character, left and right accessory view shifts up.
First, I trimmed the string that shown on title label or subtitle label. After, concatenate "..." as string at the end of the string.
Solution is a little bit hack, but it works like charm.
I made fix like this. Needs to polish the code, but for me it works.
- (UIView*)rightCalloutAccessoryView {
UIButton* rightButton = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
return [self parentViewForView:rightButton];
}
- (UIView*)parentViewForView:(UIView*)view {
#ifdef __IPHONE_8_0
CGRect rr = view.frame;
rr.origin.x = 2.0f;
rr.origin.y = (64.0f - rr.size.height ) / 2.0f;
view.frame = rr;
UIView *result = [[UIView alloc] initWithFrame:CGRectMake(0, 0, rr.size.width + 4.0f, 64.0f)];
[result addSubview:view];
return result;
#else
return view;
#endif
}
If you want to customize the leftCalloutAccessoryView be implemented an customizedView that you have to refine the width been under 225.0f ( the better setup is 220.0f ) on iPhone 4, 4S, 5, 5+.
for example :
UIView *_customContentView = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 225.0f, 38.0f)];
view.leftCalloutAccessoryView = _customContentView;
That's my solved experience.
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];
}
}