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];
}
}
Related
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)
}
I have a mapview in xcode, which is all working well.
What my page does just now is like this:
downloads a bunch of data and locations from a backend database
populates a mapview with locations and drops pins
populates a table underneath the mapview
That all works great, and I end up with a mapview with a load of pins, and a tableview that has the details of those pins.
What I want to do now, is allow the user to tap on a row from the tableview, and have the map zoom and centre to the corresponding map pin, and then automatically activate the annotation pin callout.
In my 'didselectrow' method, I have the following:
MKCoordinateSpan span = MKCoordinateSpanMake(0.1f, 0.1f);
CLLocationCoordinate2D coordinate = { [item.latitude floatValue], [item.longitude floatValue] };
MKCoordinateRegion region = { coordinate, span };
[self.mapView setRegion:region animated:YES];
This works great too. Tapping on the table row will zoom to and centre the map pin at this location.
I just can't get the last step of firing the annotation pin callout to work.
I have tried:
[mapview annotationsInMapRect:mapview.visibleMapRect];
But this isn't working, and it is possible that there still might be 2 or 3 map pins in the visible area.
What I need to do is to get the pin nearest to the centred location (see above - item.latitude / item.longitude) to automatically open it's callout.
Everything in the code is set up and working, and the map pins have callouts that fire when tapped on, I just need this last stage of having the pin nearest the centre location to open automatically.
Can anyone help with this?
I have tried various other suggestions on SO, but none seem to fit this requirement.
I think I have got solution for your problem you need to use this [_mapView setSelectedAnnotations:#[[[self.mapView annotations] lastObject]]];
For testing I have created an small project that have these 2 methods.
- (IBAction)buttonTouched:(id)sender {
[_mapView showAnnotations:[self.mapView annotations] animated:YES];
[self performSelector:#selector(showAnnotationCallOut) withObject:nil afterDelay:1.0f];
}
- (void) showAnnotationCallOut {
[_mapView setSelectedAnnotations:#[[[self.mapView annotations] lastObject]]];
}
Note: I have called just one annotation for test that why I am calling last object. You'll need to call it for specific annotation of your annotation array.
Edit: According to Richerd's comment here is solution for problem of finding the annotion and showing the callout fro that.
for (MapViewAnnotation *annotion in [self.mapView annotion]) {
if ([annotion.identifire isEqualToString:annotationToCallCallOutIdentifier]) {
//[_mapView setSelectedAnnotations:#[annotation]];
[_mapView selectAnnotation:annotation animated:YES];
break;//don't break if there are can be more than one callouts
}
}
I have a MKPinAnnotationView that is always in the center of the map. When panning and zooming
the pin gives me the center coordinates (lat/long) of the map.
Currently when you zoom in, it just zooms into wherever your directing the map to zoom into.
I'd really like to lock the zoom onto the pin.
Any ideas on how I'd achieve this?
Assuming by MKPinAnnotation you mean MKPinAnnotationView, you can access the annotation's coordinate and use that to create a region and subsequently set the region of the mapView to that region centered on the coordinate:
MKCoordinateRegion region = MKCoordinateRegionMake(pin.annotation.coordinate, MKCoordinateSpanMake(.05, .05));
[self.mapView setRegion:region animated:YES];
I am using a MKUserTrackingBarButtonItem button to allow the user to automatically track their location on a map. The problem is that when they tap this button, it is zoomed too far out. I want it to start at a specified zoom level (i.e. span). How can I achieve this?
When the user taps the button to change to MKUserTrackingModeFollow, it seems to use the same zoom level that the user last manually changed to (i.e. using gestures on the map). Attempting to specify a different zoom level via setRegion or setVisibleMapRect does not affect what zoom level will be used when the mode is changed to MKUserTrackingModeFollow.
Attempting to override mapView:didChangeUserTrackingMode: to set the region causes the mode to be changed back to MKUserTrackingModeNone. Example:
- (void)mapView:(MKMapView *)mapView didChangeUserTrackingMode:(MKUserTrackingMode)mode animated:(BOOL)animated {
if (mode == MKUserTrackingModeFollow) {
CLLocationCoordinate2D center = mapView.userLocation.location.coordinate;
MKCoordinateSpan span = MKCoordinateSpanMake(0.002306, 0.001717);
[mapView setRegion:MKCoordinateRegionMake(center, span) animated:YES];
// [mapView setUserTrackingMode:MKUserTrackingModeFollow animated:NO];
}
}
If I attempt to reset the mode immediately after setting the region, it works fine if the user is stationary, but zooms back out if the user is moving.
The simplest solution would be if there was a way to simply specify something like a zoom level for MKUserTraking by sending it my span value. However, since that doesn't seem to exist, what else can I do?
I had the same issue and used a different approach to fix it. You can use the MapCamera function for this instead of that button.
On each new location do this:
MKMapCamera *newCamera = [MKMapCamera cameraLookingAtCenterCoordinate:[newLocation coordinate]
fromEyeCoordinate:[oldLocation coordinate]
eyeAltitude:2000];
[mapView setCamera:newCamera animated:TRUE];
And play with the eyeAltitude.
If the user manually zooms in or out you can read the altitude value from mapview.camera.altitude also don't update the camera when the user is manually using the map.
According to apple documentation used here
https://developer.apple.com/reference/mapkit/mkmapview/1616208-usertrackingmode
Setting the tracking mode to follow or followWithHeading causes the map view to center the map on that location and begin tracking the user’s location. If the map is zoomed out, the map view automatically zooms in on the user’s location, effectively changing the current visible region.
Here changing the region does not effect your visible region due to that reason.
- (void)mapView:(MKMapView *)mapView didChangeUserTrackingMode:(MKUserTrackingMode)mode animated:(BOOL)animated {
if (mode == MKUserTrackingModeFollow) {
CLLocationCoordinate2D center = mapView.userLocation.location.coordinate;
MKCoordinateSpan span = MKCoordinateSpanMake(0.002306, 0.001717);
[mapView setRegion:MKCoordinateRegionMake(center, span) animated:YES];
// [mapView setUserTrackingMode:MKUserTrackingModeFollow animated:NO];
}
}
So you just need to change center coordinate on didChangeUserTrackingMode instead of changing the whole region
- (void)mapView:(MKMapView *)mapView didChangeUserTrackingMode:(MKUserTrackingMode)mode animated:(BOOL)animated {
if (mode == MKUserTrackingModeFollow) {
[self.mapView setCenterCoordinate:mapView.userLocation.location.coordinate animated:YES];
}
}
- (void)mapView:(MKMapView *)mapView didUpdateUserLocation:(MKUserLocation *)userLocation {
[self.mapView setCenterCoordinate:mapViewuserLocation.location.coordinate animated:YES];
}
on click of MKUserTrackingBarButtonItem change the zoom level
CLLocationCoordinate2D center = mapView.userLocation.location.coordinate;
MKCoordinateSpan span = MKCoordinateSpanMake(0.002306, 0.001717);
[mapView setRegion:MKCoordinateRegionMake(center, span) animated:YES];
I am implementing showuserlocation=TRUE property to show a blue dot on map.
Clicking on blue dot will produce a callout with title Current Location.
I am referring the Default Maps App on iPhone. I have applied the Headings to it so it rotates to show North same as Maps App.
But when my map rotates as with Heading, Current Location Callout also rotates.
I want it to stay at one place as in iPhone Map App.
If you want callout to be vertical at all times
- (void)locationManager:(CLLocationManager *)manager didUpdateHeading:(CLHeading *)newHeading
{
for (id<MKAnnotation> annotation in mapView.annotations)
{
NSLog(#"keep callout vertical");
MKAnnotationView* annotationView = [mapView viewForAnnotation:annotation];
[annotationView setTransform:CGAffineTransformMakeRotation(0)];
}
}