MapKit annotation view labels changing position/showing in wrong annotation when dequeued - ios

I'm adding labels just underneath my map pins to show some detail of the pin. I'm doing this by adding a UILabel as a subview of the annotationView and setting it's number of lines to 0 in order to display multiple lines on the same label. I'm having an issue where the map is initially loading the correct annotations and I can see the correct data in the labels for the map pin, but when I navigate to a different screen and then go back to the map, the map pins are all in the right places but the labels beneath them no longer show the correct data in the label, they all get mixed up. The map pins start showing data belonging to other map pins. Also if I move the map around a bit and then go back to the same pin, same thing happens.
I'm adding data to the label in the viewForAnnotation method:
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation{
// Return the view marker different for editing items ie draggable undragrable.
if ([annotation isKindOfClass:[PSMapAnnotation class]] == YES) {
PSMapAnnotation *senderAnnotation = (PSMapAnnotation *)annotation;
PSMapAnnotationView *annotationView = (PSMapAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:[senderAnnotation getPinKey]];
if (annotationView == nil) {
annotationView = [[PSMapAnnotationView alloc]
initWithAnnotation:senderAnnotation
reuseIdentifier:[senderAnnotation getPinKey]];
// THIS LINE RETURNS A STRING ARRAY CONTAINING LABEL DATA
NSMutableArray *taggedAnswers = [self _getTaggedAnswersForMarkerLabel:annotationView];
annotationView.enabled = YES;
annotationView.canShowCallout = NO;
UILabel *annotationLabel = [[UILabel alloc] init];
annotationLabel.text = #"";
annotationLabel.numberOfLines = 0;
annotationLabel.font = [UIFont fontWithName:#"HelveticaNeue" size:14.0];
annotationLabel.textColor = [UIColor blackColor];
annotationLabel.textAlignment = NSTextAlignmentCenter;
// LOOP THROUGH STRING ARRAY AND ADD ALL STRINGS TO ANNOTATION LABEL
for (NSString* taggedAnswer in taggedAnswers)
{
NSString *textToShow = [NSString stringWithFormat: #"%#\n", taggedAnswer];
annotationLabel.text = [annotationLabel.text stringByAppendingString: textToShow];
}
[annotationLabel sizeToFit];
annotationLabel.center = CGPointMake(annotationView.center.x, annotationView.center.y);
annotationView.myLabel = annotationLabel;
[annotationView addSubview:annotationLabel];
[annotationView setAnnotation:annotation];
}
else {
[annotationView setAnnotation:annotation];
}
return annotationView;

All of your code for configuring the annotation view is inside the if (annotationView == nil) { ... } block. So, if it successfully reused an annotation view (your else block), you’re not setting the label.
You should move the annotationLabel.text = ... code outside of this if statement. E.g.
if (annotationView == nil) {
// instantiate annotation view and add subview, but don’t set the `text` of the label yet
} else {
annotationView.annotation = annotation;
}
annotationView.annotationLabel.text = ...
For example
// CustomAnnotationView.h
#import UIKit;
#import MapKit;
#interface CustomAnnotationView : MKPinAnnotationView
#end
And
// CustomAnnotationView.m
#import "CustomAnnotationView.h"
#interface CustomAnnotationView ()
#property (nonatomic) UILabel *label;
#end
#implementation CustomAnnotationView
- (instancetype)initWithAnnotation:(id<MKAnnotation>)annotation reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithAnnotation:annotation reuseIdentifier:reuseIdentifier];
if (self) {
self.enabled = YES;
self.canShowCallout = NO;
self.label = [[UILabel alloc] init];
self.label.translatesAutoresizingMaskIntoConstraints = false;
self.label.text = #"";
self.label.numberOfLines = 0;
self.label.font = [UIFont fontWithName:#"HelveticaNeue" size:14.0];
self.label.textColor = [UIColor labelColor];
self.label.textAlignment = NSTextAlignmentCenter;
[self addSubview:self.label];
[NSLayoutConstraint activateConstraints:#[
[self.label.topAnchor constraintEqualToAnchor:self.bottomAnchor constant:8],
[self.label.centerXAnchor constraintEqualToAnchor:self.centerXAnchor constant:-self.centerOffset.x]
]];
[self updateForAnnotation:annotation];
}
return self;
}
- (void)setAnnotation:(id<MKAnnotation>)annotation {
[super setAnnotation:annotation];
[self updateForAnnotation:annotation];
}
- (void)updateForAnnotation:(id<MKAnnotation>)annotation {
self.label.text = annotation.title;
}
#end
That yields:
Clearly customize this as you see fit, but hopefully it illustrates the idea.
Note the use of constraints to make sure that the label is always centered.
Probably needless to say, here I am using the title property of the annotation, but you could check for your annotation subclass and grab whatever property you want, e.g. your textToShow. But given that there is already a property for associating a text string with an annotation, either title (and optionally subtitle, if you need it), I would use that.
You might consider moving the adding of the label into the PSMapAnnotationView init method, so the annotation view takes care of configuring its subviews. And you can override the setter of the annotation property for PSMapAnnotationView to set the label text for you.
This has the benefit of (a) it simplifies your MKMapViewDelegate code; (b) keeps subview configuration where it belongs; and (c) opens up the door for completely eliminating viewForAnnotation if only supporting iOS 11 and later. E.g. in viewDidLoad add a line that says:
[self.mapView registerClass:[CustomAnnotationView class] forAnnotationViewWithReuseIdentifier:MKMapViewDefaultAnnotationViewReuseIdentifier];
And then you can remove viewForAnnotation entirely (unless you need support for multiple annotation types).

Related

MKMarkerAnnotationView Doesn't Show

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:

dynamically change leftCalloutAccessoryView based on the MKAnnotationView that is selected

I have an array of images, that are associated with each Annotation on my map. I can statically add an image to the leftCalloutAccessoryView but I am unsure how to make this dynamic. I hope its clear what I am asking. Each annotation has its own individual image that I want to display but I am unsure of how to reference the image in the following method;
- (MKAnnotationView *)mapView:(MKMapView *)mv viewForAnnotation:(id <MKAnnotation>)annotation
{
if([annotation isKindOfClass:[MKUserLocation class]])
return nil;
NSString *annotationIdentifier = #"PinViewAnnotation";
MyAnnotationView *pinView = (MyAnnotationView *) [mv dequeueReusableAnnotationViewWithIdentifier:annotationIdentifier];
if (!pinView)
{
pinView = [[MyAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:annotationIdentifier];
pinView.canShowCallout = YES;
UIImageView *houseIconView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"Icon"]];//static image
[houseIconView setFrame:CGRectMake(0, 0, 30, 30)];
pinView.leftCalloutAccessoryView = houseIconView;
}
else
{
pinView.annotation = annotation;
}
return pinView;
}
My array "self.sandwiches" contains Sandwich objects that have a name (NSString) and an imageName ('NSString').
Im looking for a solution where I can get the index of the pin that is selected, similar to a UITableView where you can get its index, and access it from the array using indexPath.row.
My Annotation class;
.h
#import
#import
#import
#interface SandwichAnnotation : NSObject<MKAnnotation>
#property (nonatomic,assign) CLLocationCoordinate2D coordinate;
#property (nonatomic,copy) NSString * title;
#property (nonatomic,copy) NSString * subtitle;
#end
.m
#import "SandwichAnnotation.h"
#implementation SandwichAnnotation
#synthesize coordinate,title,subtitle;
#end
In viewForAnnotation, rather than "getting the index of the pin" (which would work but is less efficient here than with a UITableView), I suggest adding the data required to the annotation class itself.
This way, the data is more self-contained and the code in the delegate method or elsewhere doesn't need to worry, know, or be kept in sync with where or what kind of structure the annotation object is stored in. As long as you have a reference to the annotation object, you will immediately have all the data needed for that annotation (or at least it will contain references to the related data within itself).
The viewForAnnotation delegate method provides a reference to the annotation object it needs a view for (the annotation parameter). It's typed generically as id<MKAnnotation> but it is actually an instance of the exact type that was created (either SandwichAnnotation by you or MKUserLocation by the map view).
One option is to make the parent Sandwich class itself implement MKAnnotation and eliminate the SandwichAnnotation class. This way, no searching or references are needed at all since the annotation parameter will actually be a Sandwich.
However, you may want to keep a separate class for your annotation objects (which is fine). In this case, you can add a reference to the parent object(s) in the annotation class. Example:
#interface SandwichAnnotation : NSObject<MKAnnotation>
#property (nonatomic,assign) CLLocationCoordinate2D coordinate;
#property (nonatomic,copy) NSString * title;
#property (nonatomic,copy) NSString * subtitle;
#property (nonatomic,retain) Sandwich * whichSandwich; // <-- add reference
#end
When creating a SandwichAnnotation, set the reference to which Sandwich the annotation is for:
for (Sandwich *currentSandwich in self.sandwiches) {
SandwichAnnotation *sa = [[SandwichAnnotation alloc] init...];
sa.coordinate = ...
sa.title = ...
sa.whichSandwich = currentSandwich; // <-- set reference
[mapView addAnnotation:sa];
}
Finally, in viewForAnnotation, if annotation is of type SandwichAnnotation, set the leftCalloutAccessoryView:
- (MKAnnotationView *)mapView:(MKMapView *)mv viewForAnnotation:(id <MKAnnotation>)annotation
{
if (! [annotation isKindOfClass:[SandwichAnnotation class]]) {
//If annotation is not a SandwichAnnotation, return default view...
//This includes MKUserLocation.
return nil;
}
//At this point, we know annotation is of type SandwichAnnotation.
//Cast it to that type so we can get at the custom properties.
SandwichAnnotation *sa = (SandwichAnnotation *)annotation;
NSString *annotationIdentifier = #"PinViewAnnotation";
MyAnnotationView *pinView = (MyAnnotationView *) [mv dequeueReusableAnnotationViewWithIdentifier:annotationIdentifier];
if (!pinView)
{
pinView = [[MyAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:annotationIdentifier];
pinView.canShowCallout = YES;
//Here, just initialize a blank UIImageView ready to use.
//Set image below AFTER we have a dequeued or new view ready.
UIImageView *houseIconView = [[UIImageView alloc] init];
[houseIconView setFrame:CGRectMake(0, 0, 30, 30)];
pinView.leftCalloutAccessoryView = houseIconView;
}
else
{
pinView.annotation = annotation;
}
//At this point, we have a dequeued or new view ready to use
//and pointing to the correct annotation.
//Update image on the leftCalloutAccessoryView here
//(not just when creating the view otherwise an annotation
//that gets a dequeued view will show an image of another annotation).
UIImageView *houseIconView = (UIImageView *)pinView.leftCalloutAccessoryView;
NSString *saImageName = sa.whichSandwich.imageName;
UIImage *houseIcon = [UIImage imageNamed: saImageName];
if (houseIcon == nil) {
//In case the image was not found,
//set houseIcon to some default image.
houseIcon = someDefaultImage;
}
houseIconView.image = houseIcon;
return pinView;
}

Having trouble passing UIImage to MKPinAnnotationView

I subclassed MKPointAnnotation and added a NSString and a UIImage so I can pass these elements to an MKPinAnnotationView. The string passes into the MKPinAnnotationView fine, but the image attribute is coming through as null. I can't figure out why. I will say that one time running this code on my phone the image appeared on a pin once, so maybe I have a memory leak? The other 1000 times I've compiled the images show as null in the log and do not appear. Any help is appreciated.
Here is my subclass customMapAnnotation.h:
#interface customMapAnnotation : MKPointAnnotation
#property (strong, nonatomic) NSString *restaurantID;
#property (strong, nonatomic) UIImage *restaurantIcon;
#end
Here is where my customMapAnnotation is created. I'm using Parse.com so there is a statement here to convert a PFFile to a UIImage. If I check the log I confirm that foodPoint.restaurantIcon has a value when this is ran.
customMapAnnotation *foodPoint = [[customMapAnnotation alloc] init];
foodPoint.coordinate = spot;
foodPoint.restaurantID = [object objectId];
foodPoint.title = [object objectForKey:#"restaurantName"];
foodPoint.subtitle = [object objectForKey:#"cuisineType"];
leftIcon = [object objectForKey:#"restaurantImage"];
[leftIcon getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
if (!error) {
image = [UIImage imageWithData:data];
// image can now be set on a UIImageView
foodPoint.restaurantIcon = image;
}
}];
[self.mainMap addAnnotation:foodPoint];
Now here is all of my code for my MKPinAnnotationView. Notice I run an NSLog to confirm if fp.restaurantIcon has a value, and it is null. I know this use of NSLog is not graceful, but it should return the memory location of the image. It comes back null. Again, my other custom attribute fp.restaurantId is working fine.
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation {
static NSString *identifier = #"RoutePinAnnotation";
if (annotation == mapView.userLocation)
{
return nil;
}
else
{
MKPinAnnotationView *pinView = [[MKPinAnnotationView alloc]
initWithAnnotation:annotation reuseIdentifier:identifier];
pinView.animatesDrop=NO;
pinView.canShowCallout=YES;
UIButton *rightButton = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
//cast annotation parameter to our class so compiler understands...
customMapAnnotation *fp = (customMapAnnotation *)annotation;
NSLog(fp.restaurantIcon);
//get restaurantID from the annotation parameter ("fp")...
[rightButton setTitle:fp.restaurantID forState:UIControlStateNormal];
[rightButton addTarget:self
action:#selector(buttonMethod:)
forControlEvents:UIControlEventTouchUpInside];
pinView.rightCalloutAccessoryView = rightButton;
//NSLog(fp.restaurantIcon);
UIImageView *profileIconView = [[UIImageView alloc] initWithImage:fp.restaurantIcon];
profileIconView.frame = CGRectMake(0,0,31,31);
pinView.leftCalloutAccessoryView = profileIconView;
return pinView;
}
}
Here is my didSelectAnnotationView:
- (void) mainMap:(MKMapView *)mainMap didSelectAnnotationView:(MKAnnotationView *)view
{
if ([view.annotation isKindOfClass:[chwMapAnnotation class]])
{
chwMapAnnotation *fp = (chwMapAnnotation *)view.annotation;
UIImageView *profileIconView = [[UIImageView alloc] initWithImage:fp.restaurantIcon];
profileIconView.frame = CGRectMake(0,0,31,31);
view.leftCalloutAccessoryView = profileIconView;
}
}
I agree with the comment by #Vincent that since the images are being loaded in the background asynchronously, they have not yet been loaded when viewForAnnotation is called and so the annotation's callout is stuck with a blank left icon.
When the image loading is finished by getDataInBackgroundWithBlock, there is no automated signal telling the annotation view to refresh its leftCalloutAccessoryView.
A crude workaround in this case would be to manually force the refresh of the leftCalloutAccessoryView when the annotation is selected (which is when the callout will be displayed containing the leftCalloutAccessoryView).
When an annotation is selected, the map view calls the didSelectAnnotationView delegate method. Here, the crude workaround can be applied:
-(void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view
{
if ([view.annotation isKindOfClass:[customMapAnnotation class]])
{
customMapAnnotation *fp = (customMapAnnotation *)view.annotation;
UIImageView *profileIconView = [[UIImageView alloc] initWithImage:fp.restaurantIcon];
profileIconView.frame = CGRectMake(0,0,31,31);
view.leftCalloutAccessoryView = profileIconView;
}
}
Of course, it's possible that the image still hasn't loaded for the annotation that was selected (it's either just taking very long or the image url is invalid, etc).
What you can do is in viewForAnnotation, if fp.restaurantIcon is nil, set the left icon to some generic "loading" icon that you add to the project resources. In didSelectAnnotationView, you can also first check if fp.restaurantIcon is still nil and, if so, do nothing (ie. leave the icon as the generic "loading" icon).
A less crude solution might be to create a custom annotation view class that listens for an "image finished loading" message from its associated annotation and refreshes itself automatically.
I would say don't try to subclass MKPointAnnotation, create your own object that conforms to the MKAnnotation protocol, and use annotation views that are NOT MKPinAnnotationViews.

Custom MKAnnotation callout bubble with button

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];
}
}

custom data variables in mkannotation

I have a map view controller (UIViewController, MKMapView), with its delegate (HCIResultMapViewController).
I wish to have following functionality in this part.
1). I wish to use my custom made NSObject , so that I can associate others details along with the basic entities like title, subtitle etc.
Hence according to my needs I coded as following
In HCIResultMapViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
_houseList = [[_resultList objectForKey:#"result"] objectForKey:#"listings"];
// NSLog([_houseList description]);
int i = 0;
for (NSDictionary *house in _houseList) {
HCIAnnotationViewController *annotation = [[HCIAnnotationViewController alloc]
initwithHouse:house];
[_mapView addAnnotation:annotation];
// NSLog(#"asdjhasdjsajdhaksdjghasdasdjahsdahskvdka");
self.currIdentifier = i;
i++;
}
[_mapView setShowsUserLocation:NO];
}
The other delegate functions
-(void) mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control {
for (NSObject<MKAnnotation> *annotation in _mapView.selectedAnnotations) {
NSLog(#"hellomaster");
NSLog(annotation.title);
if ([annotation isKindOfClass:[HCIAnnotationViewController class]]) {
NSLog(#"hellomaster");
}
}
The last one
-(MKAnnotationView*) mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation {
NSString *identifier = #"currIdentifier";
MKPinAnnotationView *annotationView =
(MKPinAnnotationView *)[_mapView dequeueReusableAnnotationViewWithIdentifier:identifier];
if (annotationView == nil) {
annotationView = [[MKPinAnnotationView alloc]
initWithAnnotation:annotation
reuseIdentifier:identifier];
} else {
annotationView.annotation = annotation;
}
annotationView.enabled = YES;
annotationView.canShowCallout = YES;
annotationView.tag = self.currIdentifier;
// Create a UIButton object to add on the
UIButton *rightButton = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
[rightButton setTitle:annotation.title forState:UIControlStateNormal];
[annotationView setRightCalloutAccessoryView:rightButton];
/*
UIButton *leftButton = [UIButton buttonWithType:UIButtonTypeInfoLight];
[leftButton setTitle:annotation.title forState:UIControlStateNormal];
[annotationView setLeftCalloutAccessoryView:leftButton];
*/
return annotationView;
}
But I see that the class equivalence fails. Can you tell me what I'm doing wrong?
I think what I want, in simple words, is, how can I send some data (NSDictionary*) along with a annotation such that I can retrieve it whenever I want?
Please dont tag this as repeated question or so. I have tried many questions, googling etc. but couldn't find a suitable solution for this.
Here You can also set NSMutableDictionary instand of NSString.
Create custom AnnotationView:
#import <MapKit/MapKit.h>
#interface AnnotationView : MKPlacemark
#property (nonatomic, readwrite, assign) CLLocationCoordinate2D coordinate;
#property (nonatomic, strong) NSString *title; //Here You cam set `NSMutableDictionary` instand of `NSString`
#property (nonatomic, strong) NSString *subtitle; //Here You cam set `NSMutableDictionary` instand of `NSString`
#end
And in .m file
#import "AnnotationView.h"
#implementation AnnotationView
- (id)initWithCoordinate:(CLLocationCoordinate2D)coordinate addressDictionary:(NSDictionary *)addressDictionary
{
if ((self = [super initWithCoordinate:coordinate addressDictionary:addressDictionary]))
{
self.coordinate = coordinate;
}
return self;
}
#end
// Use Annotation Add #import "AnnotationView.h" in your relevant .m file:
CLLocationCoordinate2D pCoordinate ;
pCoordinate.latitude = LatValue;
pCoordinate.longitude = LanValue;
// Create Obj Of AnnotationView class
AnnotationView *annotation = [[AnnotationView alloc] initWithCoordinate:pCoordinate addressDictionary:nil] ;
annotation.title = #"I m Here";
annotation.subtitle = #"This is Sub Tiitle";
[self.mapView addAnnotation:annotation];
I couldn't find exact way to implement custom data variables or the reason why my class equivalence fails. But I figured out a way to over come this kind of situation. This might be redundant in method, but still works like a charm.
So the idea is to use the tagging system in control button. I observed that every time I send a message to my map view to add a annotation it immediately calls my viewforannotation method. Since I'm iterating through an array, I maintained a index global pointer, with which I set tag of the control button. Later when the button is clicked, I retrieve the tag and use it to get the object I want.
Anyone else have any alternative or direct solution, please do post.

Resources