MKMapView viewForAnnotation: returning nil - objective-c

I'm trying to remove a pin from a map. I have an observer on the #"selected" property of the MKPinAnnotationView so I know which object to delete. When the user taps the trash can icon and a pin is selected, this method gets called:
- (IBAction)deleteAnnotationView:(id)sender {
MKPinAnnotationView *pinView = (MKPinAnnotationView *)[self.mapView viewForAnnotation:self.currentAddress];
[pinView removeObserver:self forKeyPath:#"selected"];
[self.mapView removeAnnotation:self.currentAddress];
[self.map removeLocationsObject:self.currentAddress];
}
This method works fine if I do not drag the pin anywhere. If I drag the pin, my pinView in the above method returns nil, and the MKPinAnnotationView never gets removed from the MKMapView. I'm not sure why. Here's the didChangeDragState delegate method:
- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view didChangeDragState:(MKAnnotationViewDragState)newState fromOldState:(MKAnnotationViewDragState)oldState {
if (newState == MKAnnotationViewDragStateEnding) {
CLLocationCoordinate2D draggedCoordinate = view.annotation.coordinate;
CLGeocoder *geocoder = [[CLGeocoder alloc] init];
CLLocation *location = [[CLLocation alloc] initWithLatitude:draggedCoordinate.latitude longitude:draggedCoordinate.longitude];
[geocoder reverseGeocodeLocation:location completionHandler:^(NSArray *placemarks, NSError *error) {
// Check for returned placemarks
if (placemarks && [placemarks count] > 0) {
CLPlacemark *topResult = [placemarks objectAtIndex:0];
AddressAnnotation *anAddress = [AddressAnnotation annotationWithPlacemark:topResult inContext:self.managedObjectContext];
view.annotation = anAddress;
self.currentAddress = anAddress;
}
}];
}
}
In both the didChangeDragState: and deleteAnnotationView: methods, my self.address object has a valid address. For some reason though, when the pin is dragged, the pinView is nil. Any thoughts? Thanks!

Observing an annotation view's selected property via KVO should be unnecessary since there's the didSelectAnnotationView delegate method and (even better in your case) the selectedAnnotations property in MKMapView.
Assuming the user taps the trash can after selecting a pin, the trash can tap method can get the currently selected annotation through the selectedAnnotations property. For example:
if (mapView.selectedAnnotations.count == 0)
{
//No annotation currently selected
}
else
{
//The currently selected annotation is the first object in the array...
id<MKAnnotation> ann = [mapView.selectedAnnotations objectAtIndex:0];
//do something with ann...
}
In the above example, there was no need to access the annotation's view, no observer and no need for your own "currentAddress" property.
If instead you want to do some action immediately when an annotation is selected, you can put the code in the didSelectAnnotationView delegate method. There, the annotation selected is view.annotation.
Regarding the issue on the drag-end, the current code is completely replacing the view's annotation. I think this is only a good idea when the view is being created or re-used in the viewForAnnotation delegate method. In the drag-end method, you should instead try updating the view.annotation's properties instead of replacing with an entirely new object.

Related

Asynchronously setting MKAnnotationView wrong images

I'm asynchronously loading images via AWS S3 and setting the MkMapView image to this image. My s3 code is long but it works everywhere else-and I think I narrowed this down to a #synchronized problem. I'm using this because in my didSelectAnnotationView, I'm rearranging my NSMutableArray which requires thread safety.
My steps are as follows. First, whenever the map screen changes, I download the users in that area and parse the JSON repsonse. I set these responses to an NSMutableArray via
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
#synchronized(self.trainerArray){
[self.trainerArray removeAllObjects];
for(NSDictionary* item in tempDict){
FSTrainer* tempTrainer = [[FSTrainer alloc] initWith:item];
//adding to temp array and map
[self.trainerArray addObject:tempTrainer];
TrainerPin* trainerPin = [[TrainerPin alloc] initWith:tempTrainer];
[self.mapView addAnnotation:trainerPin];
}
}
}
Now when I set this via the viewForAnnotation (with custom reuse identifiers) this all works fine, UNLESS I zoom really fast or erratically, and then the same image gets set twice (the first one).
- (MKAnnotationView *) mapView: (MKMapView *) mapView viewForAnnotation: (id) annotation {
TrainerPin* trainerPinForView = (TrainerPin*)annotation;
TrainerMapImage *pin = (TrainerMapImage *) [self.mapView dequeueReusableAnnotationViewWithIdentifier: [NSString stringWithFormat:#"trainerAnnotation%ld", (long)trainerPinForView.trainer.id]];
if (!pin) {
pin = [[TrainerMapImage alloc] initWithAnnotation: trainerPinForView reuseIdentifier: [NSString stringWithFormat:#"myPin%ld", (long)trainerPinForView.trainer.id]];
} else {
pin.annotation = trainerPinForView;
}
//custom method in TrainerMapImage
[pin asyonchronouslySetImage];
return pin;
}
Another thing to note is that wrapping this in a GCD queue ALWAYS returns the first image for every MKMapView

Change MKAnnotationView programmatically

Is there a way to change MKAnnotationView style (like from red label with number to green colored label with number).
I want to change this style according to distance from target. My annotation is moving, with user.
I dont want to use remove / add annotation, because it causes "blinking".
Can it be done someway?
UPDATE:
I am adding code, how I am doing it right now
MKAnnotationView *av = [mapView viewForAnnotation:an];
if ([data->type isMemberOfClass:[UserAnnotationImage class]])
{
UIImage *img = [UIImage imageNamed: ((UserAnnotationImage *)data->type)->url];
[av setImage:img];
}
else if ([data->type isMemberOfClass:[UserAnnotationLabel class]])
{
UIView * v = [av viewWithTag:0];
v = ((UserAnnotationLabel *)data->type)->lbl;
av.frame = ((UserAnnotationLabel *)data->type)->lbl.frame;
}
else if ([data->type isMemberOfClass:[UserAnnotationView class]])
{
UIView * v = [av viewWithTag:0];
v = ((UserAnnotationView *)data->type)->view;
av.frame = ((UserAnnotationView *)data->type)->view.frame;
}
Sadly, its not working :(
Yes, basically you get a reference to the annotation view and update its contents directly.
Another way, if you have a custom annotation view class, is to have the annotation view monitor the changes it is interested in (or have something outside tell it) and update itself.
The first approach is simpler if you are using a plain MKAnnotationView or MKPinAnnotationView.
Wherever you detect that a change to the view is needed, get a reference to the view by calling the map view's viewForAnnotation instance method. This is not the same as calling the viewForAnnotation delegate method.
Once you have a reference to the view, you can modify as needed and the changes should appear immediately.
An important point is that the logic you use to update the view outside the delegate method and the logic you have in the viewForAnnotation delegate method must match. This is because the delegate method may get called later (after you've updated the view manually) by the map view and when it does, the code there should take the updated data into account.
The best way to do that is to have the annotation view construction code in a common method called both by the delegate method and where you update the view manually.
See change UIImage from MKAnnotation in the MKMapView for an example that updates just the annotation view's image.
For an example (mostly an idea for an approach) of updating the view using a custom annotation view class, see iPad Mapkit - Change title of "Current Location" which updates the view's pin color periodically (green, purple, red, green, purple, red, etc).
There are too many unknowns in your code to explain why it doesn't work. For example:
What is data? Is it annotation-specific (is it related to an)? What is type? Does it change after the annotation has been added to the map?
Why is data storing entire view objects like a UILabel or UIView instead of just the underlying data that you want to show in those views?
imageNamed requires the image to be a resource in the project (not any arbitrary url)
Don't use a tag of 0 (that's the default for all views). Start numbering from 1.
You get a view using viewWithTag but then replace it immediately with another view.
I'll instead give a more detailed but simple(r) example...
Assume you have an annotation class (the one that implements MKAnnotation) with these properties (in addition to coordinate, title, and subtitle):
#property (nonatomic, assign) BOOL haveImage;
#property (nonatomic, copy) NSString *labelText;
#property (nonatomic, copy) NSString *imageName;
#property (nonatomic, assign) CLLocationDistance distanceFromTarget;
To address the "important point" mentioned above (that the viewForAnnotation delegate method and the view-update-code should use the same logic), we'll create a method that is passed an annotation view and configures it as needed based on the annotation's properties. This method will then be called both by the viewForAnnotation delegate method and the code that manually updates the view when the annotation's properties change.
In this example, I made it so that the annotation view shows the image if haveImage is YES otherwise it shows the label. Additionally, the label's background color is based on distanceFromTarget:
-(void)configureAnnotationView:(MKAnnotationView *)av
{
MyAnnotationClass *myAnn = (MyAnnotationClass *)av.annotation;
UILabel *labelView = (UILabel *)[av viewWithTag:1];
if (myAnn.haveImage)
{
//show image and remove label...
av.image = [UIImage imageNamed:myAnn.imageName];
[labelView removeFromSuperview];
}
else
{
//remove image and show label...
av.image = nil;
if (labelView == nil)
{
//create and add label...
labelView = [[[UILabel alloc]
initWithFrame:CGRectMake(0, 0, 50, 30)] autorelease];
labelView.tag = 1;
labelView.textColor = [UIColor whiteColor];
[av addSubview:labelView];
}
if (myAnn.distanceFromTarget > 100)
labelView.backgroundColor = [UIColor redColor];
else
labelView.backgroundColor = [UIColor greenColor];
labelView.text = myAnn.labelText;
}
}
The viewForAnnotation delegate method would look like this:
-(MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation
{
if ([annotation isKindOfClass:[MyAnnotationClass class]])
{
static NSString *myAnnId = #"myann";
MKAnnotationView *av = [mapView dequeueReusableAnnotationViewWithIdentifier:myAnnId];
if (av == nil)
{
av = [[[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:myAnnId] autorelease];
}
else
{
av.annotation = annotation;
}
[self configureAnnotationView:av];
return av;
}
return nil;
}
Finally, the place where the annotation's properties change and where you want to update the annotation view, the code would look something like this:
ann.coordinate = someNewCoordinate;
ann.distanceFromTarget = theDistanceFromTarget;
ann.labelText = someNewText;
ann.haveImage = YES or NO;
ann.imageName = someImageName;
MKAnnotationView *av = [mapView viewForAnnotation:ann];
[self configureAnnotationView:av];

Updating MKannotation image without flashing

I want to update the images of some of my annotations on a mapview every 5 seconds, however I dont' want to remove and re-add them to the map as this causes them to 'flash' or refresh, (ie disapear then reappear). I want it to be seamless.
I've tried the following:
//get the current icon
UserAnnotation *annotation = [self GetUserIconWithDeviceId:deviceId];
//make a new annotation for it
UserAnnotation *newAnnotation = [[UserAnnotation alloc]
initWithCoordinate: userCoordinates
addressDictionary:nil];
newAnnotation.title = name;
newAnnotation.userDeviceId = deviceId;
NSInteger ageIndicator = [[userLocation objectForKey: #"ageIndicator"] integerValue];
newAnnotation.customImage = [UserIconHelpers imageWithAgeBorder:ageIndicator FromImage: userImage];
//if its not there, add it
if (annotation != nil){
//move it
//update location
annotation.coordinate = userCoordinates;
//add new one
[self.mapView addAnnotation: newAnnotation];
//delete old one
[self.mapView removeAnnotation: annotation];
} else {
//just addd the new one
[self.mapView addAnnotation: newAnnotation];
}
as a thought that if I added the new icon on top I could then remove the old icon, but this still caused the flashing.
Has anyone got any ideas?
In the case where the annotation is not nil, instead of adding and removing, try this:
annotation.customImage = ... //the new image
MKAnnotationView *av = [self.mapView viewForAnnotation:annotation];
av.image = annotation.customImage;
Swift version of Anna answer:
annotation.customImage = ... //the new image
let av = self.mapView.viewForAnnotation(dnwl!)
av?.image = annotation.customImage
It seems you are using your own custom views for the annotations, in that case you can simply add a "refresh" method to your custom view and call it after you have updated the underlying annotation (ie: a custom view -a derived class from MKAnnotationView- is always attached to a potentially custom "annotation" class that conforms to the MKAnnotation protocol)
*) CustomAnnotationView.h
#interface CustomAnnotationView : MKAnnotationView
{
...
}
...
//tell the view to re-read the annotation data it is attached to
- (void)refresh;
*) CustomAnnotationView.m
//override super class method
- (void)setAnnotation:(id <MKAnnotation>)annotation
{
[super setAnnotation:annotation];
...
[self refresh];
}
- (void)refresh
{
...
[self setNeedsDisplay]; //if necessary
}
*) Where you handle the MKMapView and its annotations
for(CustomAnnotation *annotation in [m_MapView annotations])
{
CustomAnnotationView *annotationView = [m_MapView viewForAnnotation:annotation];
[annotationView refresh];
}

toolbarSelectableItemIdentifiers: is not called

I'm trying to make selectable NSToolbarItems. I've connected everything correctly in IB, but toolbarSelectableItemIdentifiers: is not working. It doesn't get called. The delegate is the File's Owner (subclass of NSWindowController), and the toolbar is in a sheet. Here's my code:
// TOOLBAR DLGT
- (NSArray *)toolbarSelectableItemIdentifiers:(NSToolbar *)toolbar {
NSLog(#"Foo");
NSMutableArray *arr = [[NSMutableArray alloc] init];
for (NSToolbarItem *item in [toolbar items]) {
[arr addObject:[item itemIdentifier]];
}
return [arr autorelease];
}
Screenshot:
Can you help me please?
No, I don't want to use BWToolkit.
Are you positive the toolbar's delegate outlet points to the class (or instance thereof) you think it does? Are any other NSToolbar delegate methods called there (easy enough to test)?

ViewForAnnotation is not being called

I am trying to make a map, where I can see my current location, and see what the street is called.
so far, I am able to put a pin on my map, but for some reason, I am not getting the callout.
and I have put a NSLog in my viewForAnnotation method, but it is not being called, so i wasn't able to test it.
can someone help me?
-(void)lat:(float)lat lon:(float)lon
{
CLLocationCoordinate2D location;
location.latitude = lat;
location.longitude = lon;
NSLog(#"Latitude: %f, Longitude: %f",location.latitude, location.longitude);
//One location is obtained.. just zoom to that location
MKCoordinateRegion region;
region.center=location;
//Set Zoom level using Span
MKCoordinateSpan span;
span.latitudeDelta=.005f;
span.longitudeDelta=.005f;
region.span=span;
[map setRegion:region animated:TRUE];
//MKReverseGeocoder *geocoder=[[MKReverseGeocoder alloc] initWithCoordinate:location];
//geocoder.delegate=self;
//[geocoder start];
if (cPlacemark != nil) {
[map removeAnnotation:cPlacemark];
}
cPlacemark=[[CustomPlacemark alloc] initWithCoordinate:location];
cPlacemark.title = mPlacemark.thoroughfare;
cPlacemark.subtitle = mPlacemark.locality;
[map addAnnotation:cPlacemark];
[cPlacemark release];
[mLocationManager stopUpdatingLocation];
}
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation {
if ([annotation isKindOfClass:[MKUserLocation class]])
return nil;
// try to dequeue an existing pin view first
if ([annotation isKindOfClass:[CustomPlacemark class]]){
MKPinAnnotationView *pinView=(MKPinAnnotationView *)[map dequeueReusableAnnotationViewWithIdentifier:#"customIdentifier"];
if (!pinView)
{
// if an existing pin view was not available, create one
MKPinAnnotationView* cPinAnnoView = [[[MKPinAnnotationView alloc]
initWithAnnotation:annotation reuseIdentifier:#"customIdentifier"] autorelease];
cPinAnnoView.pinColor = MKPinAnnotationColorPurple;
cPinAnnoView.animatesDrop = YES;
cPinAnnoView.canShowCallout = YES;
// Add button
UIButton *leftButton = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
[leftButton addTarget:self action:#selector(annotationViewClick:) forControlEvents:UIControlEventTouchUpInside];
cPinAnnoView.leftCalloutAccessoryView = leftButton;
} else
{
pinView.annotation = annotation;
}
return pinView;
}
return nil;
}
Right now I have customized my viewForAnnotation to be like this.
But I still can't get a callout from my pin and the pin remains red.
But it should be purple of nothing at all
I had the same problem which was not setting the MapView delegate to the File Owner.
Open your nib
Right click on the MapView
Drag the delegate to the File's Owner
I had the same problem, as you mentioned. The delegate had been set to ViewController, but the viewForAnnotation selector was not being called. After some checks, I realized if you do not call addAnotation in the main thread, mapView would not call viewForAnnotation, so following update resolved my problem:
Before:
[_mMapView addAnnotation:marker];
After:
dispatch_async(dispatch_get_main_queue(), ^{
[_mMapView addAnnotation:marker];
});
In order to get the viewForAnnotation to be called, add mapView.delegate=self; to e.g. the viewDidLoad method:
- (void)viewDidLoad {
[super viewDidLoad];
mapView.delegate=self;
}
Could it be that your annotation has been added outside the current view area of the MKMapView?
For storyboard, Ctl drag the MKMapView to the orange circle on the bottom bar of ViewController, and select delegate.
This will solve the problem.
As vatrif mentioned in the comments, you must set your delegate BEFORE adding annotations to your MKMapView object.
Others have already explained, odds are high you have not connected your mapview delegate to your controller. Its the first thing to check
i have been working in ios 9 Mapview related app and I experienced the same problem.
somehow I solved my problem, in my case im resizing the mapview.
I added delegate after i resize the mapview. it works now perfectly.!
After having set the delegate for the mapview if still the viewforannotation not getting called then this is something which you have missed - set the self.mapView.showsUserLocation to YES, in interface builder you can tick the shows userLocation option in attributes inspector.