Center MKMapView BEFORE displaying callout - ios

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

Related

MKAnnotationView deselected on setRegion

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!

How to drop 500 pins at once on a MapView

I have around 500 pins I would like to display on a MKMapView.
However, when I call mapView.addAnnotations(places) (places being an array of MKAnnotation objects), the pins slowly drop one by one.
I would like to either :
drop all pins at once at load
cancel the drop animation altogether
Is this possible ?
You should set animatesDrop property of your MKAnnotationViews to NO(false)
By setting animation property to No will drop all the Pins at once on the MapView.
MKAnnotationView *annotationView =[[MKAnnotationView alloc]init];
annotationView.animatesDrop=FALSE;
You'll need to implement your own drop animation in the didAddAnnotationViews delegate method. You should also set animatesDrop to NO to avoid a possible double animation.
- (void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)annotationViews
{
NSTimeInterval delayInterval = 0;
for (MKAnnotationView *annView in annotationViews)
{
CGRect endFrame = annView.frame;
annView.frame = CGRectOffset(endFrame, 0, -500);
[UIView animateWithDuration:0.125
delay:delayInterval
options:UIViewAnimationOptionAllowUserInteraction
animations:^{ annView.frame = endFrame; }
completion:NULL];
delayInterval += 0.0625;
}
}

Automatic annotation callouts after some zoom level

I would like to display annotation callouts automatically after a specific zoom level. However, I do not want to have all the annotation callouts, but only those which are being shown in the screen.
To display annotation callout of those that are visible on screen just the code below should do the trick, as it relies on the selectAnnotation method of MKMapView, of course, after detecting the desired zoom level:
for (MKAnnotation *annotation in mapView.annotations) {
if ( MKMapRectContainsPoint(mapView.visibleMapRect, MKMapPointForCoordinate(annotation.coordinate)) ) {
[mapView selectAnnotation:annotation animated:YES];
}
}

Map View always center map on my location

I use the following code to get my location when a user presses a button
[mapview setShowsUserLocation:YES];
and then the follwoing to center the map to te user's location
- (void)mapView:(MKMapView *)mapView didUpdateUserLocation:(MKUserLocation *)userLocation
{
[mapView setCenterCoordinate:mapView.userLocation.location.coordinate animated:YES];
}
My question is how to prevent the map from always centering on my location? I want to allow the user to pan the map.
Center on the location only the first time you show the map. Here is some pseudo code...
- (void)mapView:(MKMapView *)mapView didUpdateUserLocation:(MKUserLocation *)userLocation
{
if(shouldCenterLocation){
[mapView setCenterCoordinate:mapView.userLocation.location.coordinate animated:YES];
shouldCenterLocation = FALSE;
}
//do all your other stuff here
}
shouldCenterLocation is a boolean flag that you can set to TRUE the first time the map is shown, then set it to FALSE until you exit the view (or any other condition you have for showing the center location).
edit: you can toggle the state of shouldCenterLocation in the same method that you handle the button press.
I think it came in with iOS5, you can now drop the delegate stuff and just set the userTrackingMode of the MKMapView. Set it to MKUserTrackingModeFollow to make the make move along with the user and then when they start panning the map around it'll turn off the tracking mode automatically, you then just need to provide a button to turn it back on.
On swift 3 I prefer use animated method:
mapView.setUserTrackingMode(.follow, animated: true)
but set property good worked:
mapView.userTrackingMode = .follow

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