Ch1llb4y
Ch1llb4y

Reputation: 281

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.

incorrect callout position

How can I center the map BEFORE the callout's position is being rendered and displayed?

Upvotes: 4

Views: 1452

Answers (3)

mrcrowley
mrcrowley

Reputation: 99

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)
}

Upvotes: 0

Grifas
Grifas

Reputation: 217

Here an other solution :

  1. Create a new boolean property var selectFirstAnnotation = false in your controller

  2. Set it to true before to center the annotation

  3. 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

Upvotes: 1

Greg
Greg

Reputation: 41

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.

Upvotes: 4

Related Questions