4

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?

Ch1llb4y
  • 331
  • 2
  • 13
  • Have you tried setting up an `MKCoordinateRegion` with its longitude and latitude set the same as the pin's, and a span set to whatever the current span is, and then setting the `center` property of the region to the pin's location? – GlennRay Dec 22 '15 at 00:17
  • @GlennRay this would just center the map at the pin, and also only after the callout was displayed, if I understand correctly. – Ch1llb4y Dec 22 '15 at 20:17
  • Looking at other SO questions like this one very quickly, two solutions pop up. First, you could use dispatch to delay the appearance of the callout for a few seconds, giving the map time to center. Or you could have the map center on a location x%/y% away from where the pin actually is, in order to give room for the callout. – GlennRay Dec 23 '15 at 02:20
  • @GlennRay Okay, but how do I delay this callout apperance? As I know, it pops up automatically before `mapView(mapView: MKMapView, didSelectAnnotationView view: MKAnnotationView)` is called, where you can perform stuff like map moving etc. – Ch1llb4y Dec 23 '15 at 08:46
  • in http://stackoverflow.com/questions/10047596/showing-callout-after-moving-mapview , Jacob K suggested: `dispatch_time_t dt = dispatch_time(DISPATCH_TIME_NOW, 0.2 * NSEC_PER_SEC); dispatch_after(dt, dispatch_get_main_queue(), ^(void) { [mapView setCenterCoordinate:view.annotation.coordinate animated:YES]; });` – GlennRay Dec 24 '15 at 01:45
  • @GlennRay Yes, this is what I currently use, and it still only centers on the annotation but not on the callout which is not shown completely then. – Ch1llb4y Dec 24 '15 at 15:45
  • @Ch1llb4y did you find a solution for it? I'm having the same issue and I can't fix it. Thanks!! – vicente.fava May 16 '16 at 15:33
  • @Ch1llb4y: Any solution found for this? I am stuck with the same issue. – Lohith Korupolu May 09 '17 at 12:31

3 Answers3

4

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.

Greg
  • 41
  • 3
1

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

Grifas
  • 217
  • 4
  • 15
0

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)
}
mrcrowley
  • 101
  • 7