MKAnnotationView deselected on setRegion - ios

I am trying to zoom in significantly on a pin when a MKAnnotationView is clicked with MapkKit using the following code:
MKCoordinateRegion mapRegion;
mapRegion.center = view.annotation.coordinate;;
mapRegion.span.latitudeDelta = 0.2;
mapRegion.span.longitudeDelta = 0.2;
[MKMapView animateWithDuration:0.15 animations:^{
[mapView setRegion:mapRegion animated: YES];
}];
However, whenever I zoom in I want the pin to remain selected. Is there a way to prevent the MKAnnotatiotionView from being deselected and the function didDeselectAnnotationView from being called.
I think the reason it might be happening is because the mapView on the zoom is updating the annotations. Is there a way to prevent this from happening if this is the cause?

Yes, if [mapView setRegion: ...] causes the annotations on the mapView to change for whatever reason, then your selected annotation will be deselected (because it's about to be removed!).
One way to fix this is to do a 'diff' replace of your annotations. For example, at the moment you might have some code that looks like (expressed in Swift):
func displayNewMapPins(pinModels: [MyCustomPinModel]) {
self.mapView.removeAnnotations(self.mapView.annotations) //remove all of the currently displayed annotations
let newAnnotations = annotationModels.map { $0.toAnnotation } //convert 'MyCustomPinModel' to an 'MKAnnotation'
self.mapView.addAnnotations(newAnnotations) //put the new annotations on the map
}
You want to change it to be more like this:
func displayNewMapPins(pinModels: [MyCustomPinModel]) {
let oldAnnotations = self.mapView.annotations
let newAnnotations = annotationModels.map { $0.toAnnotation }
let annotationsToRemove = SomeOtherThing.thingsContainedIn(oldAnnotations, butNotIn: newAnnotations)
let annotationsToAdd = SomeOtherThing.thingsContainedIn(newAnnotations, butNotIn: oldAnnotations)
self.mapView.removeAnnotations(annotationsToRemove)
self.mapView.addAnnotations(annotationsToAdd)
}
The exact implementation of SomeOtherThing.thingsContainedIn(:butNotIn:) depends on your requirements, but this is the general code structure you want to aim for.
Doing it this way will have an added benefit of improving the performance of your app - adding and removing annotations from a MKMapView can be really expensive!

Related

Center MKMapView BEFORE displaying callout

I am trying to center an MKMapView after an annotation was selected. I also have enabled canShowCallout but it seems that iOS is first displaying the callout (which is shifted when it would not fit in the screen) and then the map is being moved, resulting in the callout being not completely visible on the screen.
How can I center the map BEFORE the callout's position is being rendered and displayed?
I wanted to accomplish the same thing and ended up doing the following.
A word of caution before I begin: I know the solution is pretty ugly!...but hey, it works.
Note: I am targeting iOS 9 but it should work on prior versions of iOS:
Okay, here we go:
first off, create a new property in your view controller, e.g.: #property(nonatomic, assign, getter=isPinCenteringOngoing) BOOL pinCenteringOngoing;
in mapView:viewForAnnotation: set canShowCallout to NO for your annotationViews
in mapView:didSelectAnnotationView: do the following:
- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view
{
if([view isKindOfClass:$YOURANNOTATIONVIEWCLASS$.class])
{
if(!self.isPinCenteringOngoing)
{
self.pinCenteringOngoing = YES;
[self centerMapOnSelectedAnnotationView:($YOURANNOTATIONVIEWCLASS$ *)view];
}
else
{
self.pinCenteringOngoing = NO;
}
}
}
in mapView:didDeselectAnnotationView: do the following:
- (void)mapView:(MKMapView *)mapView didDeselectAnnotationView:(MKAnnotationView *)view
{
if([view isKindOfClass:$YOURANNOTATIONVIEWCLASS$.class])
{
if(!self.isPinCenteringOngoing)
{
view.canShowCallout = NO;
}
}
}
and finally create a new method that does the actual work:
- (void)centerMapOnSelectedAnnotationView:($YOURANNOTATIONVIEWCLASS$ *)view
{
// Center map
CGPoint annotationCenter = CGPointMake(CGRectGetMidX(view.frame), CGRectGetMidY(view.frame));
CLLocationCoordinate2D newCenter = [self.mapView convertPoint:annotationCenter toCoordinateFromView:view.superview];
[self.mapView setCenterCoordinate:newCenter animated:YES];
// Allow callout to be shown
view.canShowCallout = YES;
// Deselect and then select the annotation so the callout is actually displayed
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.3 * NSEC_PER_SEC), dispatch_get_main_queue(), ^(void)
{
[self.mapView deselectAnnotation:view.annotation animated:NO];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^(void)
{
[self.mapView selectAnnotation:view.annotation animated:NO];
});
});
}
To complete my answer, here is a textual explanation of what I'm doing in the code above and why I'm doing it:
What I want is the annotation to be centered on screen, and the callout to be centered above it.
What I get by default is:
When selecting an annotation, the map opens the callout, and if necessary adjusts the map so the callout fits on screen. By no mean does that standard implementation guarantee, that the callout is "centered" above the annotation.
By centering the map with setCenterCoordinate:, the annotation view is centered on the map.
Now the two previous points combined can result in the callout to be "cut off" as the annotation is centered on the map, but the callout is not centered above the annotation.
To fix this, I do the following:
first I disable the callout to be displayed by default, setting canShowCallout to NO for every annotationView
when the user selects an annotation, I first center the map
I then allow the callout to be shown, setting canShowCallout to YES for the selected annotation
I then deselect and then again select the annotation, so the callout is actually displayed
in order for the callout to be correctly centered above the annotation, I need to do the deselecting/selecting somewhat delayed so that the map centering can complete
I hope my answer may prove useful.
Here an other solution :
Create a new boolean property var selectFirstAnnotation = false in your controller
Set it to true before to center the annotation
Add this is in regionDidChangeAnimated.
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
if selectFirstAnnotation == true {
if let annotation = mapView.annotations.first(where: { !($0 is MKUserLocation) }) {
mapView.selectAnnotation(annotation, animated: true)
selectFirstAnnotation = false
}}}
Works fine for my behaviour
I tried both previous solutions and Greg's is the correct answer with a couple of tweaks... I put the map centering in and animation block to slow down the animation.
UIView.animate(withDuration: 0.8) {
self.mapView.setCenter(CLLocationCoordinate2D(latitude: newCenter.latitude, longitude: newCenter.longitude), animated: true)
}
Then I was getting an unacceptable blip from the separation of the deselect and select calls into different dispatches with different times and discovered they can both go in the same dispatch. Adding animated: true to the select call adds a nice touch as well.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.9) {
mapView.deselectAnnotation(view.annotation, animated: false)
mapView.selectAnnotation(view.annotation!, animated: true)
}

MKAnnotation not updating position

I am using the HGMovingAnnotation and HGMovingAnnotationView code off of github to animate a MKAnnotation on an MKmap. When I run the example project from HG project everything works fine.
I have altred the original HG project to allow me to manually push a new coordinate to the HGMapPath and then move the annotation where I want it.
I have placed a button, for testing, to run the manual process and everything works fine. The annotation moves around the screen. The issue is, when I try to now call this manual method with data from a live socket.io connection, the map annotation won't move.
Also, when the map first loads the annotation won't show up until I move the map a little bit. The same thing for the moving annotation manually, it won't show the movement from the stream of data, until I zoom the map. But if I do the push button way, avoiding the io stream, the annotation moves without needing to zoom or pan the map?
PLACING THE VIEW ANNOTATIONS
if(doubleLat && doubleLng) {
CLLocationCoordinate2D coordinate = CLLocationCoordinate2DMake(doubleLat, doubleLng);
//Create path object
self.assignedAmbPath = [[HGMapPath alloc] initWithCoordinate:coordinate];
HGMovingAnnotation *movingObject = [[HGMovingAnnotation alloc] initWithMapPath:self.assignedAmbPath];
self.movingAssignedAmbObject = movingObject;
// add the annotation to the map
[self.mapView addAnnotation:movingObject];
// zoom the map around the moving object
MKCoordinateSpan span = MKCoordinateSpanMake(0.01, 0.01);
MKCoordinateRegion region = MKCoordinateRegionMake(MKCoordinateForMapPoint(self.movingAssignedAmbObject.currentLocation), span);
[self.mapView setRegion:region animated:YES];
// start moving the object
[movingObject start];
}
CODE THAT WORKS
- (IBAction)testMoveBtnPressed:(id)sender {
//TODO: move x and y
DLog(#"============== Test move button was pressed ================ ");
NSLog(#"");
int randn = (random() % 15)+15;
float pscale = (float)randn / 10000;
double lat = 39.9813855 + pscale;
double lng = -75.1502155 + pscale;
for (id<MKAnnotation> annotation in self.mapView.annotations){
MKAnnotationView* anView = [self.mapView viewForAnnotation: annotation];
if (![annotation isKindOfClass:[PhoneAnnotation class]]){
// Process annotation view
[((HGMovingAnnotation *)annotation) trackToNewPosition:CLLocationCoordinate2DMake(lat, lng)];
}
}
}
CODE THAT DOESN'T WORK
{
//TODO: move thing to new location
double doubleLat = [lat doubleValue];
double doubleLng = [lng doubleValue];
// NSLog(#"--------------- Jason it is ------------- Latitude being passed in is %f", doubleLat);
// NSLog(#"--------------- Jason it is ------------- Longitude being passed in is %f", doubleLng);
//
// [self.movingAssignedAmbObject trackToNewPosition:CLLocationCoordinate2DMake(doubleLat, doubleLng)];
for (id<MKAnnotation> annotation in self.mapView.annotations){
MKAnnotationView* anView = [self.mapView viewForAnnotation: annotation];
if (![annotation isKindOfClass:[PhoneAnnotation class]]){
// Process annotation view
[((HGMovingAnnotation *)annotation) trackToNewPosition:CLLocationCoordinate2DMake(doubleLat, doubleLng)];
}
}
}
The issue is the HG library though works as described doesn't work proper, unless your using the path, if you don't create the annotation with a coordinate attached, which it doesn't.

How do you set initial size in iOS UIMapkit

I want my mapView to display at the initial size (zoom level I determine) with phone location being centred in the map. Once this is done I want the user to be able to change zoom levels and pan to their hearts content. Next time they come into the app I want to reinitialise the map the same as the previous time.
Problem is when I come in it seems to set the map size before it has got a valid location fix.
Can anyone point me at an example that does best practice initialisation?
I solve this problem in next way. I think code is self-explained
- (void)mapView:(MKMapView *)mapView
didUpdateUserLocation:(MKUserLocation *)userLocation
{
if (!_userLocated &&
userLocation.coordinate.latitude != 0.0 &&
userLocation.coordinate.longitude != 0.0)
{
MKCoordinateRegion mapRegion;
mapRegion.center = mapView.userLocation.coordinate;
mapRegion.span = MKCoordinateSpanMake(0.04, 0.04);
[mapView setRegion:mapRegion animated: YES];
_userLocated = YES;
}
}
And dont forget to set UIMapView delegate and Shows user location.

iOS SDK Mapview Annotation callout redraw error

I'm trying to add the distance from the user's position to a selected annotation's subtitle in a mapview. The mechanics of it are working, but the actual callout gets messed up the first time it's displayed. There appears to be a redraw problem.
Subsequent taps on the pin show the correct layout.
Here's the relevant code:
// called when selecting annotations
- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view{
MKPointAnnotation *selectedAnnotation = view.annotation;
//attempt to add distance on annotation
CLLocation *pointALocation = [[CLLocation alloc]
initWithLatitude:selectedAnnotation.coordinate.latitude
longitude:selectedAnnotation.coordinate.longitude];
float distanceMeters = [pointALocation distanceFromLocation:locationManager.location];
//for sending info to detail
myPinTitle = selectedAnnotation.title;
[selectedAnnotation setSubtitle:[NSString stringWithFormat:#"%.2f miles away", (distanceMeters / 1609.344)]];
}
I've tried calling [view setNeedsDisplay], but to no avail.
Thanks in advance for your help.
The Solution that Worked
Here's the solution I finally came up with. It seems to work.
I edited out the duplicate code from the didSelectAnnotationView method, above, and came up with:
//called when user location changes
- (void)updatePinsDistance
{
for (int x=0; x< [[mapView annotations]count]; x++) {
MKPointAnnotation *thisPin =[[mapView annotations] objectAtIndex:x];
//attempt to add distance on annotation
CLLocation *pointALocation = [[CLLocation alloc]
initWithLatitude:thisPin.coordinate.latitude
longitude:thisPin.coordinate.longitude];
float distanceMeters = [pointALocation distanceFromLocation:locationManager.location];
NSString *distanceMiles = [NSString stringWithFormat:#"%.2f miles from you",
(distanceMeters / 1609.344)];
[thisPin setSubtitle:distanceMiles];
}
}
You should set your subtitle in another place than didSelectAnnotationView. Actually all annotationViews should have their title and subtitle set before they are returned by the mapView:viewForAnnotation: method.
The fact that you set a long subtitle certainly explains that the callout is not the right size. The size must be calculated before the didSelectAnnotationView is called.

iOS - MKMapView show annotation only a certain zoom level

I have a MKMapView with some custom annotations that don't look that great when the map is zoom far out.
Is it possible to only show/add them when the map is at a certain zoom level?
Using Marko's answer I came to this solution.
Everytime region changes, I change the ViewController's property isAtBigZoom.
func mapView(mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
isAtBigZoom = mapView.region.span.latitudeDelta < 0.01
}
Then at didSet of the property, I execute this code.
var isAtBigZoom = false {
didSet {
// this guard ensures, that the showing and hiding happens only once
guard oldValue != isAtBigZoom else {
return
}
// in my case I wanted to show/hide only a certain type of annotations
for case let annot as MapTextAnnotation in mapView.annotations {
mapView.viewForAnnotation(annot)?.alpha = isAtBigZoom ? 1 : 0
}
}
}
If you also want to start with the annotations hidden, just add the alpha changing code to viewForAnnotation method.
Works great and I haven't noticed big issues with performance. Though that may change with the increasing number of annotations...
You can get the map zoom level via
[map region];
property of the MKMapView. also you get the notifications for region changing events
by implementing the MKMapViewDelegate method and setting the delegate
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated
here you can check what your current zoom level is. I don't recommend removing or adding all the annotations while zooming / panning since that could really effect the app performance. I haven't really tried setting alpha to 0.0 or hidden property on MKAnnotationView, but that could be your best bet.

Resources