MKCircle is not updating radius but it's translating - ios

I've to draw an MKCicle into an MKMapView. Then I've to re-draw it when user, through a slider, change the radius. I remove it and I re-create it, re-adding it to the map.
But instead of do what I'm expecting, I see the MKCircle translating over the map, maintaining the same size.
Here's my code:
- (MKOverlayView *)mapView:(MKMapView *)map viewForOverlay:(id)overlay
{
MKOverlayView* overlayView = nil;
if(overlay == self.circle)
{
//if we have not yet created an overlay view for this overlay, create it now.
if(nil == self.circleView)
{
self.circleView = [[[MKCircleView alloc] initWithCircle:self.circle] autorelease];
self.circleView.fillColor = [UIColor blueColor];
self.circleView.strokeColor = [UIColor blueColor];
self.circleView.alpha = 50;
self.circleView.lineWidth = 2;
}
overlayView = self.circleView;
}
return overlayView;
}
-(void)drawPolygonWithLocation
{
[self.mapView removeOverlay: self.circle];
MKCoordinateRegion region;
region.center.latitude = self.geofenceLocation.latitude;
region.center.longitude = self.geofenceLocation.longitude;
region.span.latitudeDelta = 0.005;
region.span.longitudeDelta = 0.005;
MKCoordinateRegion adjustedRegion = [self.mapView regionThatFits: region];
[self.mapView setRegion:adjustedRegion animated:TRUE];
self.radius = (double)(slRadius.value);
NSLog(#"Raggio: %f", self.radius);
NSLog(#"Lat: %f, Lon: %f", region.center.latitude, region.center.longitude);
self.circle = [MKCircle circleWithCenterCoordinate:self.geofenceLocation.coordinate radius: self.radius];
NSLog(#"CIRCLE: radius %f Lat: %f, Lon: %f", self.circle.radius, self.circle.coordinate.latitude, self.circle.coordinate.longitude);
[self.mapView addOverlay:self.circle];
}
-(IBAction)updateRadius:(id)sender
{
[self drawPolygonWithLocation];
}
The NSLog is writing into the console right values, the center doesn't change and the radius changes according to the user input.
But, again, the MKCircle translates going on the north-west.
Thanks in advance,
Samuel Rabini

Fixed.
I just add
self.circleView = nil;
before the
[self.mapView addOverlay:self.circle];
in this way it works fine.
Samuel

Related

How to change MKCircle radius on mapview with animation?

I want to change the circle radius with animation like this:
https://www.youtube.com/watch?v=j0s5DUnEy3k
Here's my code:
-(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations {
if (locations != nil) {
userLocation = locations.firstObject;
CLLocationDegrees lat = 0.05;
CLLocationDegrees lon = 0.05;
MKCoordinateSpan span = MKCoordinateSpanMake(lat, lon);
CLLocationCoordinate2D location = CLLocationCoordinate2DMake(userLocation.coordinate.latitude, userLocation.coordinate.longitude);
MKCoordinateRegion region = MKCoordinateRegionMake(location, span);
[self.myMapView setRegion:region animated:true];
MKCircle *circle = [MKCircle circleWithCenterCoordinate:userLocation.coordinate radius:1000];
[self.myMapView addOverlay:circle];
}
}
I create user's location annotation and add a circle at user's location center
- (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id <MKOverlay>)overlay
{
MKCircleRenderer *circleView = [[MKCircleRenderer alloc] initWithOverlay:overlay];
circleView.strokeColor = [UIColor redColor];
circleView.fillColor = [[UIColor redColor] colorWithAlphaComponent:0.3];
circleView.lineWidth = 0.2;
return circleView;
}
and customized the circle at rendererForOverlay function
So, how can I change the circle radius with animation?
Anyone have idea to do this?

MKMapView won't respond to touching or pinching

So I have a MKMapVie object and it is set up as follows:
self.mapView = [[MKMapView alloc] initWithFrame:CGRectMake(0, 0,
[Screen width],
[Screen height])];
self.mapView.mapType = MKMapTypeSatellite;
self.mapView.userInteractionEnabled = YES;
self.mapView.scrollEnabled = YES;
self.mapView.showsUserLocation = YES;
[self addSubview:mapView];
And I set its location to be the current user location with the following method.
- (void) locateUser {
float z2 = 0.003 * 4.0;
MKCoordinateSpan span = MKCoordinateSpanMake(z2, z2);
[self.mapView setRegion: MKCoordinateRegionMake(currentLocation, span) animated:YES];
}
However once the map zooms to the users location it will not allow me to move the map around or pinch or zoom. Is there something i'm missing?
Thanks.

Incorrect user location shown on MKMapView after scaling the map programmatically

I have MKMapView which have UserTrackingMode = MKUserTrackingModeFollow,
and I have adding a circle overlay to show a region of the certain diameter at user location.
Also user can change the diameter of the region so I want to scale the map to insure whole region/circle is shown on that portion of the map.
The problem I have now is that scaling the map number of times by setting region results in incorrect user location annotation - it is moved from the correct location.
I cannot understand why is that happens, I see in the debugger that the mapView.userLocation property have correct coordinates.
But once new update is happens or I move the map manually - the annotation jumps to the correct place.
This is the code:
- (void)addCircleWithRadius:(double)radius andCoordinate:(CLLocationCoordinate2D)coordinate
{
_regionCircle = [MKCircle circleWithCenterCoordinate:coordinate radius:radius];
[_regionCircle setTitle:#"Region"];
MKCoordinateRegion region;
region.center.latitude = coordinate.latitude;
region.center.longitude = coordinate.longitude;
region.span.latitudeDelta = 0.00002 * _regionCircle.radius;
region.span.longitudeDelta = 0.00002 * _regionCircle.radius;
MKCoordinateRegion adjustedRegion = [self.mapView regionThatFits: region];
[self.mapView setRegion:adjustedRegion animated:TRUE];
_circleView = nil;
[self.mapView addOverlay:_regionCircle];
}
- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id <MKOverlay>)overlay
{
if(!_circleView)
{
_circleView = [[MKCircleView alloc] initWithCircle:overlay];
_circleView.strokeColor = [UIColor blueColor];;
_circleView.fillColor = [UIColor blueColor];
_circleView.alpha = 0.25;
_circleView.lineWidth = 2.0;
}
return _circleView;
}
- (IBAction)regionSliderValueChanged:(id)sender
{
[self updateRadiusCircle];
}
- (void) updateRadiusCircle
{
[self.mapView removeOverlays:self.mapView.overlays];
CLLocationCoordinate2D myCoordinate = {_currentLocation.coordinate.latitude, _currentLocation.coordinate.longitude};
[self addCircleWithRadius:self.radiusSlider.value andCoordinate:myCoordinate];
}
I have published the video on YouTube to better understand the issue.
https://www.youtube.com/watch?v=474gdjkGwJA

how can I limit the map area to only one country in iOS?

I am making an app for iOS using mapkit. I want to limit the boundaries of the map only to a specific region/country. Is there a way to do this?
There's no way to tell the map not to scroll out of a certain area. The only way I could think to do it would be to stop the user from scrolling when you hit one of your fences. The example below is written without testing or compiling at all so you may need to tweek it yourself but hopefully it'll get you started..
ViewController.h
CLLocationCoordinate2D myNorthEast, mySouthWest;
ViewController.m
-(void)viewDidLoad{
myNorthEast = CLLocationCoordinate2DMake(lat1,lon1);
mySouthWest = CLLocationCoordinate2DMake(lat2,lon2);
[super viewDidLoad];
}
-(void)mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated{
/*
/ Check if the map region is going to change outside your fence.
/ If so, programmatically set it back to the edge of your fence.
*/
if(!animated){
return; // Don't want to get stuck in a loop after you set your region below.
}
MKCoordinateRegion region = [mapView region];
// You will need to get the NE and SW points of the new region to compare
// First we need to calculate the corners of the map so we get the points
CGPoint nePoint = CGPointMake(mapView.bounds.origin.x + mapView.bounds.size.width, mapView.bounds.origin.y);
CGPoint swPoint = CGPointMake(mapView.bounds.origin.x, bounds.origin.y + mapView.bounds.size.height);
// Then transform those point into lat,lng values
CLLocationCoordinate2D neCoord;
neCoord = [mapView convertPoint:nePoint toCoordinateFromView:mapView];
CLLocationCoordinate2D swCoord;
swCoord = [mapView convertPoint:swPoint toCoordinateFromView:mapView];
/*
You will need to mess around with the lat/lon & sign of the new center calculation for the other cases.
*/
if(neCoord.latitude > myNorthEast.latitude){
MKCoordinateRegion newRegion;
newRegion.span = region.span;
CLLocationCoordinate2D newCenter;
newCenter.longitude = region.center.longitude;
newCenter.latitude = myNorthEast.latitude - region.span.latitudeDelta;
newRegion.center = newCenter;
[mapView setRegion:newRegion animated:NO];
}else if(neCoord.longitude < myNorthEast.longitude){
}else if(swCoord.latitude < mySouthWest.latitude){
}else if(swCoord.longitude > mySouthWest.longitude){
}
}
Part of this comes from this answer:
Getting the bounds of an MKMapvIew
Here is a further specification of the previous answer. Note that this will be different for different parts of the world:
if(neCoord.latitude > myNorthEast.latitude){
MKCoordinateRegion newRegion;
newRegion.span = region.span;
CLLocationCoordinate2D newCenter;
newCenter.longitude = region.center.longitude;
newCenter.latitude = myNorthEast.latitude - region.span.latitudeDelta / 2;
newRegion.center = newCenter;
[mapView setRegion:newRegion animated:NO];
} else if(swCoord.latitude < mySouthWest.latitude){
MKCoordinateRegion newRegion;
newRegion.span = region.span;
CLLocationCoordinate2D newCenter;
newCenter.longitude = region.center.longitude;
newCenter.latitude = mySouthWest.latitude + region.span.latitudeDelta / 2;
newRegion.center = newCenter;
[mapView setRegion:newRegion animated:NO];
} else if(neCoord.longitude > myNorthEast.longitude){
MKCoordinateRegion newRegion;
newRegion.span = region.span;
CLLocationCoordinate2D newCenter;
newCenter.longitude = myNorthEast.longitude - region.span.longitudeDelta / 2;
newCenter.latitude = region.center.latitude;
newRegion.center = newCenter;
[mapView setRegion:newRegion animated:NO];
} else if(swCoord.longitude < mySouthWest.longitude){
MKCoordinateRegion newRegion;
newRegion.span = region.span;
CLLocationCoordinate2D newCenter;
newCenter.longitude = mySouthWest.longitude + region.span.longitudeDelta / 2;
newCenter.latitude = region.center.latitude;
newRegion.center = newCenter;
[mapView setRegion:newRegion animated:NO];
}

Zooming MKMapView to fit annotation pins?

I am using MKMapView and have added a number of annotation pins to the map about a 5-10 kilometre area. When I run the application my map starts zoomed out to show the whole world, what is the best way to zoom the map so the pins fit the view?
EDIT:
My initial thinking would be to use MKCoordinateRegionMake and calculate the coordinate centre, longitudeDelta and latitudeDelta from my annotations. I am pretty sure this will work, but I just wanted to check I was not missing anything obvious.
Code added, BTW: FGLocation is an class that conforms to MKAnnotation, locationFake is an NSMutableArray of these objects. Comments are always welcome ....
- (MKCoordinateRegion)regionFromLocations {
CLLocationCoordinate2D upper = [[locationFake objectAtIndex:0] coordinate];
CLLocationCoordinate2D lower = [[locationFake objectAtIndex:0] coordinate];
// FIND LIMITS
for(FGLocation *eachLocation in locationFake) {
if([eachLocation coordinate].latitude > upper.latitude) upper.latitude = [eachLocation coordinate].latitude;
if([eachLocation coordinate].latitude < lower.latitude) lower.latitude = [eachLocation coordinate].latitude;
if([eachLocation coordinate].longitude > upper.longitude) upper.longitude = [eachLocation coordinate].longitude;
if([eachLocation coordinate].longitude < lower.longitude) lower.longitude = [eachLocation coordinate].longitude;
}
// FIND REGION
MKCoordinateSpan locationSpan;
locationSpan.latitudeDelta = upper.latitude - lower.latitude;
locationSpan.longitudeDelta = upper.longitude - lower.longitude;
CLLocationCoordinate2D locationCenter;
locationCenter.latitude = (upper.latitude + lower.latitude) / 2;
locationCenter.longitude = (upper.longitude + lower.longitude) / 2;
MKCoordinateRegion region = MKCoordinateRegionMake(locationCenter, locationSpan);
return region;
}
This is the one I found here that worked for me:
(EDIT: I have updated the solution using #Micah's suggestion to increase the pointRect by 0.1 to ensure the rect doesn't end up being infinitesimally small!)
MKMapRect zoomRect = MKMapRectNull;
for (id <MKAnnotation> annotation in mapView.annotations)
{
MKMapPoint annotationPoint = MKMapPointForCoordinate(annotation.coordinate);
MKMapRect pointRect = MKMapRectMake(annotationPoint.x, annotationPoint.y, 0.1, 0.1);
zoomRect = MKMapRectUnion(zoomRect, pointRect);
}
[mapView setVisibleMapRect:zoomRect animated:YES];
You could also update this to include the userLocation pin by replacing the first line with:
MKMapPoint annotationPoint = MKMapPointForCoordinate(mapView.userLocation.coordinate);
MKMapRect zoomRect = MKMapRectMake(annotationPoint.x, annotationPoint.y, 0.1, 0.1);
You've got it right.
Find your maximum and minimum latitudes and longitudes, apply some simple arithmetic, and use MKCoordinateRegionMake.
For iOS 7 and above, use showAnnotations:animated:, from MKMapView.h:
// Position the map such that the provided array of annotations are all visible to the fullest extent possible.
- (void)showAnnotations:(NSArray *)annotations animated:(BOOL)animated NS_AVAILABLE(10_9, 7_0);
Apple has added a new method for IOS 7 to simplify life a bit.
[mapView showAnnotations:yourAnnotationArray animated:YES];
You can easily pull from an array stored in the map view:
yourAnnotationArray = mapView.annotations;
and quickly adjust the camera too!
mapView.camera.altitude *= 1.4;
this won't work unless the user has iOS 7+ or OS X 10.9+ installed. check out custom animation here
I use this this code and works fine for me:
-(void)zoomToFitMapAnnotations:(MKMapView*)aMapView
{
if([aMapView.annotations count] == 0)
return;
CLLocationCoordinate2D topLeftCoord;
topLeftCoord.latitude = -90;
topLeftCoord.longitude = 180;
CLLocationCoordinate2D bottomRightCoord;
bottomRightCoord.latitude = 90;
bottomRightCoord.longitude = -180;
for(MapViewAnnotation *annotation in mapView.annotations)
{
topLeftCoord.longitude = fmin(topLeftCoord.longitude, annotation.coordinate.longitude);
topLeftCoord.latitude = fmax(topLeftCoord.latitude, annotation.coordinate.latitude);
bottomRightCoord.longitude = fmax(bottomRightCoord.longitude, annotation.coordinate.longitude);
bottomRightCoord.latitude = fmin(bottomRightCoord.latitude, annotation.coordinate.latitude);
}
MKCoordinateRegion region;
region.center.latitude = topLeftCoord.latitude - (topLeftCoord.latitude - bottomRightCoord.latitude) * 0.5;
region.center.longitude = topLeftCoord.longitude + (bottomRightCoord.longitude - topLeftCoord.longitude) * 0.5;
region.span.latitudeDelta = fabs(topLeftCoord.latitude - bottomRightCoord.latitude) * 1.1; // Add a little extra space on the sides
region.span.longitudeDelta = fabs(bottomRightCoord.longitude - topLeftCoord.longitude) * 1.1; // Add a little extra space on the sides
region = [aMapView regionThatFits:region];
[mapView setRegion:region animated:YES];
}
In Swift use
mapView.showAnnotations(annotationArray, animated: true)
In Objective c
[mapView showAnnotations:annotationArray animated:YES];
I created an extension to show all the annotations using some code from here and there in swift. This will not show all annotations if they can't be shown even at maximum zoom level.
import MapKit
extension MKMapView {
func fitAllAnnotations() {
var zoomRect = MKMapRectNull;
for annotation in annotations {
let annotationPoint = MKMapPointForCoordinate(annotation.coordinate)
let pointRect = MKMapRectMake(annotationPoint.x, annotationPoint.y, 0.1, 0.1);
zoomRect = MKMapRectUnion(zoomRect, pointRect);
}
setVisibleMapRect(zoomRect, edgePadding: UIEdgeInsets(top: 50, left: 50, bottom: 50, right: 50), animated: true)
}
}
I have converted the answer by Rafael Moreira. The credit goes to him.
For those of you looking for the Swift version, here is the code:
func zoomToFitMapAnnotations(aMapView: MKMapView) {
guard aMapView.annotations.count > 0 else {
return
}
var topLeftCoord: CLLocationCoordinate2D = CLLocationCoordinate2D()
topLeftCoord.latitude = -90
topLeftCoord.longitude = 180
var bottomRightCoord: CLLocationCoordinate2D = CLLocationCoordinate2D()
bottomRightCoord.latitude = 90
bottomRightCoord.longitude = -180
for annotation: MKAnnotation in myMap.annotations as! [MKAnnotation]{
topLeftCoord.longitude = fmin(topLeftCoord.longitude, annotation.coordinate.longitude)
topLeftCoord.latitude = fmax(topLeftCoord.latitude, annotation.coordinate.latitude)
bottomRightCoord.longitude = fmax(bottomRightCoord.longitude, annotation.coordinate.longitude)
bottomRightCoord.latitude = fmin(bottomRightCoord.latitude, annotation.coordinate.latitude)
}
var region: MKCoordinateRegion = MKCoordinateRegion()
region.center.latitude = topLeftCoord.latitude - (topLeftCoord.latitude - bottomRightCoord.latitude) * 0.5
region.center.longitude = topLeftCoord.longitude + (bottomRightCoord.longitude - topLeftCoord.longitude) * 0.5
region.span.latitudeDelta = fabs(topLeftCoord.latitude - bottomRightCoord.latitude) * 1.4
region.span.longitudeDelta = fabs(bottomRightCoord.longitude - topLeftCoord.longitude) * 1.4
region = aMapView.regionThatFits(region)
myMap.setRegion(region, animated: true)
}
Swift 3 This is de correct way for fit all annotations in map.
func zoomMapaFitAnnotations() {
var zoomRect = MKMapRectNull
for annotation in mapview.annotations {
let annotationPoint = MKMapPointForCoordinate(annotation.coordinate)
let pointRect = MKMapRectMake(annotationPoint.x, annotationPoint.y, 0, 0)
if (MKMapRectIsNull(zoomRect)) {
zoomRect = pointRect
} else {
zoomRect = MKMapRectUnion(zoomRect, pointRect)
}
}
self.mapview.setVisibleMapRect(zoomRect, edgePadding: UIEdgeInsetsMake(50, 50, 50, 50), animated: true)
}
#jowie's solution works great. One catch, if a map has only one annotation you'll end up with a fully zoomed out map. I added 0.1 to the rect make size to make sure setVisibleMapRect has something to zoom to.
MKMapRect pointRect = MKMapRectMake(annotationPoint.x, annotationPoint.y, 0.1, 0.1);
If your are looking for iOS 8 and above, the simplest way to do it is to set the var layoutMargins: UIEdgeInsets { get set } of your map view before calling func showAnnotations(annotations: [MKAnnotation], animated: Bool)
For instance (Swift 2.1):
#IBOutlet weak var map: MKMapView! {
didSet {
map.delegate = self
map.mapType = .Standard
map.pitchEnabled = false
map.rotateEnabled = false
map.scrollEnabled = true
map.zoomEnabled = true
}
}
// call 'updateView()' when viewWillAppear or whenever you set the map annotations
func updateView() {
map.layoutMargins = UIEdgeInsets(top: 25, left: 25, bottom: 25, right: 25)
map.showAnnotations(map.annotations, animated: true)
}
Added this If loop within the for loop to exclude the users location pin from this method (required in my case, and maybe others)
if (![annotation isKindOfClass:[MKUserLocation class]] ) {
//Code Here...
}
For iOS 7 and above (Referring MKMapView.h) :
// Position the map such that the provided array of annotations are all visible to the fullest extent possible.
- (void)showAnnotations:(NSArray *)annotations animated:(BOOL)animated NS_AVAILABLE(10_9, 7_0);
remark from – Abhishek Bedi
You just call:
[yourMapView showAnnotations:#[yourAnnotation] animated:YES];
var zoomRect: MKMapRect = MKMapRect.null
for annotation in mapView.annotations {
let annotationPoint = MKMapPoint(annotation.coordinate)
let pointRect = MKMapRect(x: annotationPoint.x, y: annotationPoint.y, width: 0.1, height: 0.1)
zoomRect = zoomRect.union(pointRect)
}
mapView.setVisibleMapRect(zoomRect, animated: true)
// Edited for swift 5
In Swift
var zoomRect = MKMapRectNull;
for i in 0..<self.map.annotations.count {
let annotation: MKAnnotation = self.map.annotations[i]
let annotationPoint = MKMapPointForCoordinate(annotation.coordinate);
let pointRect = MKMapRectMake(annotationPoint.x, annotationPoint.y, 0.1, 0.1);
zoomRect = MKMapRectUnion(zoomRect, pointRect);
}
self.map.setVisibleMapRect(zoomRect, animated: true)
Thanks to jowie I've updated my old category to more elegant solution. Sharing complete, almost copy&paste ready solution
MKMapView+AnnotationsRegion.h
#import <MapKit/MapKit.h>
#interface MKMapView (AnnotationsRegion)
-(void)updateRegionForCurrentAnnotationsAnimated:(BOOL)animated;
-(void)updateRegionForCurrentAnnotationsAnimated:(BOOL)animated edgePadding:(UIEdgeInsets)edgePadding;
-(void)updateRegionForAnnotations:(NSArray *)annotations animated:(BOOL)animated;
-(void)updateRegionForAnnotations:(NSArray *)annotations animated:(BOOL)animated edgePadding:(UIEdgeInsets)edgePadding;
#end
MKMapView+AnnotationsRegion.m
#import "MKMapView+AnnotationsRegion.h"
#implementation MKMapView (AnnotationsRegion)
-(void)updateRegionForCurrentAnnotationsAnimated:(BOOL)animated{
[self updateRegionForCurrentAnnotationsAnimated:animated edgePadding:UIEdgeInsetsZero];
}
-(void)updateRegionForCurrentAnnotationsAnimated:(BOOL)animated edgePadding:(UIEdgeInsets)edgePadding{
[self updateRegionForAnnotations:self.annotations animated:animated edgePadding:edgePadding];
}
-(void)updateRegionForAnnotations:(NSArray *)annotations animated:(BOOL)animated{
[self updateRegionForAnnotations:annotations animated:animated edgePadding:UIEdgeInsetsZero];
}
-(void)updateRegionForAnnotations:(NSArray *)annotations animated:(BOOL)animated edgePadding:(UIEdgeInsets)edgePadding{
MKMapRect zoomRect = MKMapRectNull;
for(id<MKAnnotation> annotation in annotations){
MKMapPoint annotationPoint = MKMapPointForCoordinate(annotation.coordinate);
MKMapRect pointRect = MKMapRectMake(annotationPoint.x, annotationPoint.y, 0.1, 0.1);
zoomRect = MKMapRectUnion(zoomRect, pointRect);
}
[self setVisibleMapRect:zoomRect edgePadding:edgePadding animated:animated];
}
#end
Hope it helps someone and thanks again jowie!
- (void)zoomMapViewToFitAnnotationsWithExtraZoomToAdjust:(double)extraZoom
{
if ([self.annotations count] == 0) return;
int i = 0;
MKMapPoint points[[self.annotations count]];
for (id<MKAnnotation> annotation in [self annotations])
{
points[i++] = MKMapPointForCoordinate(annotation.coordinate);
}
MKPolygon *poly = [MKPolygon polygonWithPoints:points count:i];
MKCoordinateRegion r = MKCoordinateRegionForMapRect([poly boundingMapRect]);
r.span.latitudeDelta += extraZoom;
r.span.longitudeDelta += extraZoom;
[self setRegion: r animated:YES];
}
As Abhishek Bedi points out in a comment, For iOS7 forward the best way to do this is:
//from API docs:
//- (void)showAnnotations:(NSArray *)annotations animated:(BOOL)animated NS_AVAILABLE(10_9, 7_0);
[self.mapView showAnnotations:self.mapView.annotations animated:YES];
For my personal project (prior to iOS7) I simply added a category on the MKMapView class to encapsulate the "visible area" functionality for a very common operation: setting it to be able to see all the currently-loaded annotations on the MKMapView instance (this includes as many pins as you might have placed, as well as the user's location). the result was this:
.h file
#import <MapKit/MapKit.h>
#interface MKMapView (Extensions)
-(void)ij_setVisibleRectToFitAllLoadedAnnotationsAnimated:(BOOL)animated;
-(void)ij_setVisibleRectToFitAnnotations:(NSArray *)annotations animated:(BOOL)animated;
#end
.m file
#import "MKMapView+Extensions.h"
#implementation MKMapView (Extensions)
/**
* Changes the currently visible portion of the map to a region that best fits all the currently loadded annotations on the map, and it optionally animates the change.
*
* #param animated is the change should be perfomed with an animation.
*/
-(void)ij_setVisibleRectToFitAllLoadedAnnotationsAnimated:(BOOL)animated
{
MKMapView * mapView = self;
NSArray * annotations = mapView.annotations;
[self ij_setVisibleRectToFitAnnotations:annotations animated:animated];
}
/**
* Changes the currently visible portion of the map to a region that best fits the provided annotations array, and it optionally animates the change.
All elements from the array must conform to the <MKAnnotation> protocol in order to fetch the coordinates to compute the visible region of the map.
*
* #param annotations an array of elements conforming to the <MKAnnotation> protocol, holding the locations for which the visible portion of the map will be set.
* #param animated wether or not the change should be perfomed with an animation.
*/
-(void)ij_setVisibleRectToFitAnnotations:(NSArray *)annotations animated:(BOOL)animated
{
MKMapView * mapView = self;
MKMapRect r = MKMapRectNull;
for (id<MKAnnotation> a in annotations) {
ZAssert([a conformsToProtocol:#protocol(MKAnnotation)], #"ERROR: All elements of the array MUST conform to the MKAnnotation protocol. Element (%#) did not fulfill this requirement", a);
MKMapPoint p = MKMapPointForCoordinate(a.coordinate);
//MKMapRectUnion performs the union between 2 rects, returning a bigger rect containing both (or just one if the other is null). here we do it for rects without a size (points)
r = MKMapRectUnion(r, MKMapRectMake(p.x, p.y, 0, 0));
}
[mapView setVisibleMapRect:r animated:animated];
}
#end
As you can see, I've added 2 methods so far: one for setting the visible region of the map to the one that fits all currently-loaded annotations on the MKMapView instance, and another method to set it to any array of objects.
So to set the mapView's visible region the code would then be as simple as:
//the mapView instance
[self.mapView ij_setVisibleRectToFitAllLoadedAnnotationsAnimated:animated];
I hope it helps =)
All the answers on this page assume that the map occupies the full screen. I actually have a HUD display (ie buttons scattered at the top and bottom) that give information ontop of the map.. and so the algorithms on the page will display the pins all right, but some of them will appear under the HUD display buttons.
My solution zooms the map in to display the annotations in a subset of the screen and works for different screen sizes (ie 3.5" vs 4.0" etc):
// create a UIView placeholder and throw it on top of the original mapview
// position the UIView to fit the maximum area not hidden by the HUD display buttons
// add an *other* mapview in that uiview,
// get the MKCoordinateRegion that fits the pins from that fake mapview
// kill the fake mapview and set the region of the original map
// to that MKCoordinateRegion.
Here is what I did in code (note: i use NSConstraints with some helper methods to make my code work in different screen sizes.. while the code is quite readable.. my answer here explains it better.. it's basically the same workflow:)
// position smallerMap to fit available space
// don't store this map, it will slow down things if we keep it hidden or even in memory
[#[_smallerMapPlaceholder] mapObjectsApplyingBlock:^(UIView *view) {
[view removeFromSuperview];
[view setTranslatesAutoresizingMaskIntoConstraints:NO];
[view setHidden:NO];
[self.view addSubview:view];
}];
NSDictionary *buttonBindingDict = #{ #"mapPlaceholder": _smallerMapPlaceholder};
NSArray *constraints = [#[#"V:|-225-[mapPlaceholder(>=50)]-176-|",
#"|-40-[mapPlaceholder(<=240)]-40-|"
] mapObjectsUsingBlock:^id(NSString *formatString, NSUInteger idx){
return [NSLayoutConstraint constraintsWithVisualFormat:formatString options:0 metrics:nil views:buttonBindingDict];
}];
[self.view addConstraints:[constraints flattenArray]];
[self.view layoutIfNeeded];
MKMapView *smallerMap = [[MKMapView alloc] initWithFrame:self.smallerMapPlaceholder.frame];
[_smallerMapPlaceholder addSubview:smallerMap];
MKCoordinateRegion regionThatFits = [smallerMap getRegionThatFits:self.mapView.annotations];
[smallerMap removeFromSuperview];
smallerMap = nil;
[_smallerMapPlaceholder setHidden:YES];
[self.mapView setRegion:regionThatFits animated:YES];
here is the code that gets region that fits:
- (MKCoordinateRegion)getRegionThatFits:(NSArray *)routes {
MKCoordinateRegion region;
CLLocationDegrees maxLat = -90.0;
CLLocationDegrees maxLon = -180.0;
CLLocationDegrees minLat = 90.0;
CLLocationDegrees minLon = 180.0;
for(int idx = 0; idx < routes.count; idx++)
{
CLLocation* currentLocation = [routes objectAtIndex:idx];
if(currentLocation.coordinate.latitude > maxLat)
maxLat = currentLocation.coordinate.latitude;
if(currentLocation.coordinate.latitude < minLat)
minLat = currentLocation.coordinate.latitude;
if(currentLocation.coordinate.longitude > maxLon)
maxLon = currentLocation.coordinate.longitude;
if(currentLocation.coordinate.longitude < minLon)
minLon = currentLocation.coordinate.longitude;
}
region.center.latitude = (maxLat + minLat) / 2.0;
region.center.longitude = (maxLon + minLon) / 2.0;
region.span.latitudeDelta = 0.01;
region.span.longitudeDelta = 0.01;
region.span.latitudeDelta = ((maxLat - minLat)<0.0)?100.0:(maxLat - minLat);
region.span.longitudeDelta = ((maxLon - minLon)<0.0)?100.0:(maxLon - minLon);
MKCoordinateRegion regionThatFits = [self regionThatFits:region];
return regionThatFits;
}
I've made a little modification of Rafael's code for MKMapView Category.
- (void)zoomToFitMapAnnotations {
if ([self.annotations count] == 0)
return;
CLLocationCoordinate2D topLeftCoord;
topLeftCoord.latitude = -90;
topLeftCoord.longitude = 180;
CLLocationCoordinate2D bottomRightCoord;
bottomRightCoord.latitude = 90;
bottomRightCoord.longitude = -180;
for (id <MKAnnotation> annotation in self.annotations) {
topLeftCoord.longitude = fmin(topLeftCoord.longitude, annotation.coordinate.longitude);
topLeftCoord.latitude = fmax(topLeftCoord.latitude, annotation.coordinate.latitude);
bottomRightCoord.longitude = fmax(bottomRightCoord.longitude, annotation.coordinate.longitude);
bottomRightCoord.latitude = fmin(bottomRightCoord.latitude, annotation.coordinate.latitude);
}
MKCoordinateRegion region;
region.center.latitude = topLeftCoord.latitude - (topLeftCoord.latitude - bottomRightCoord.latitude) * 0.5;
region.center.longitude = topLeftCoord.longitude + (bottomRightCoord.longitude - topLeftCoord.longitude) * 0.5;
region.span.latitudeDelta = fabs(topLeftCoord.latitude - bottomRightCoord.latitude) * 1.1; // Add a little extra space on the sides
region.span.longitudeDelta = fabs(bottomRightCoord.longitude - topLeftCoord.longitude) * 1.1; // Add a little extra space on the sides
[self setRegion:[self regionThatFits:region] animated:YES];
}
Based on answers above you can use universal method to zoom map to fit all annotations and overlays at the same time.
-(MKMapRect)getZoomingRectOnMap:(MKMapView*)map toFitAllOverlays:(BOOL)overlays andAnnotations:(BOOL)annotations includeUserLocation:(BOOL)userLocation {
if (!map) {
return MKMapRectNull;
}
NSMutableArray* overlaysAndAnnotationsCoordinateArray = [[NSMutableArray alloc]init];
if (overlays) {
for (id <MKOverlay> overlay in map.overlays) {
MKMapPoint overlayPoint = MKMapPointForCoordinate(overlay.coordinate);
NSArray* coordinate = #[[NSNumber numberWithDouble:overlayPoint.x], [NSNumber numberWithDouble:overlayPoint.y]];
[overlaysAndAnnotationsCoordinateArray addObject:coordinate];
}
}
if (annotations) {
for (id <MKAnnotation> annotation in map.annotations) {
MKMapPoint annotationPoint = MKMapPointForCoordinate(annotation.coordinate);
NSArray* coordinate = #[[NSNumber numberWithDouble:annotationPoint.x], [NSNumber numberWithDouble:annotationPoint.y]];
[overlaysAndAnnotationsCoordinateArray addObject:coordinate];
}
}
MKMapRect zoomRect = MKMapRectNull;
if (userLocation) {
MKMapPoint annotationPoint = MKMapPointForCoordinate(map.userLocation.coordinate);
zoomRect = MKMapRectMake(annotationPoint.x, annotationPoint.y, 0.1, 0.1);
}
for (NSArray* coordinate in overlaysAndAnnotationsCoordinateArray) {
MKMapRect pointRect = MKMapRectMake([coordinate[0] doubleValue], [coordinate[1] doubleValue], 0.1, 0.1);
zoomRect = MKMapRectUnion(zoomRect, pointRect);
}
return zoomRect;
}
And then:
MKMapRect mapRect = [self getZoomingRectOnMap:mapView toFitAllOverlays:YES andAnnotations:YES includeUserLocation:NO];
[mapView setVisibleMapRect:mapRect edgePadding:UIEdgeInsetsMake(10.0, 10.0, 10.0, 10.0) animated:YES];
Swift 5+ in 2021
// Position the map such that the provided array of annotations are all visible to the fullest extent possible.
#available(iOS 7.0, *)
open func showAnnotations(_ annotations: [MKAnnotation], animated: Bool)
so you can just:
mapView.showAnnotations(mapView.annotations, animated: true)
Just sharing my observations on this:
If you are using xCode > 6 with "inferred" sizes for the screens (see "simulated metrics" on the file inspector) in storyboard, calling
- (void)showAnnotations:(NSArray *)annotations
animated:(BOOL)animated
in viewDidLoad will result in a too large zoom level on iPhones with 4 inches because the layout for the map is still on the size of the wider screens from the storyboard.
You can move your call to showAnnotations... to viewDidAppear. Then the size of the map has already been adjusted to the smaller screen of an iPhone 4.
Or alternatively change the value "inferred" in the file inspector under "simulated metrics" to iphone 4-inch.
You can select which shapes you want to show along with the Annotations.
extension MKMapView {
func setVisibleMapRectToFitAllAnnotations(animated: Bool = true,
shouldIncludeUserAccuracyRange: Bool = true,
shouldIncludeOverlays: Bool = true,
edgePadding: UIEdgeInsets = UIEdgeInsets(top: 35, left: 35, bottom: 35, right: 35)) {
var mapOverlays = overlays
if shouldIncludeUserAccuracyRange, let userLocation = userLocation.location {
let userAccuracyRangeCircle = MKCircle(center: userLocation.coordinate, radius: userLocation.horizontalAccuracy)
mapOverlays.append(MKOverlayRenderer(overlay: userAccuracyRangeCircle).overlay)
}
if shouldIncludeOverlays {
let annotations = self.annotations.filter { !($0 is MKUserLocation) }
annotations.forEach { annotation in
let cirlce = MKCircle(center: annotation.coordinate, radius: 1)
mapOverlays.append(cirlce)
}
}
let zoomRect = MKMapRect(bounding: mapOverlays)
setVisibleMapRect(zoomRect, edgePadding: edgePadding, animated: animated)
}
}
extension MKMapRect {
init(bounding overlays: [MKOverlay]) {
self = .null
overlays.forEach { overlay in
let rect: MKMapRect = overlay.boundingMapRect
self = self.union(rect)
}
}
}
#"I'm not sure if this is because of some other factors in my implementation, but I find that showAnnotations doesn't do as close a zoom/fit of the annotations as the manual implementation does, so I've stuck with the manual one. – Ted Avery Apr 17 at 0:35"
I had the same problem, but then I tried doing showAnnotations twice (like below), and for some reason, it worked.
[mapView showAnnotations:yourAnnotationArray animated:YES];
[mapView showAnnotations:yourAnnotationArray animated:YES];
An iOS 7 compatible way is to use the following. First call showAnnotation in order to get a rectangle including all annotations. Afterwards create and UIEdgeInset with an top inset of the pin height. Thus you ensure to show the whole pin on the map.
[self.mapView showAnnotations:self.mapView.annotations animated:YES];
MKMapRect rect = [self.mapView visibleMapRect];
UIEdgeInsets insets = UIEdgeInsetsMake(pinHeight, 0, 0, 0);
[self.mapView setVisibleMapRect:rect edgePadding:insets animated:YES];
Put this in to your code accordingly:
- (void)mapView:(MKMapView *)mv didAddAnnotationViews:(NSArray *)views
{
id<MKAnnotation> mp = [annotationView annotation];
MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance([mp coordinate] ,250,250);
[mv setRegion:region animated:YES];
}

Resources