27

I'm using MapKit on iPhone. How can I know when the user changes the zoom level (zoom in\out the map)?

I've tried to use mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated; but that's called even when the map is only dragged. Unfortunately, when the map is dragged the mapView.region.span changes as well...

Help?

10x

Hiren
  • 12,720
  • 7
  • 52
  • 72
Rizon
  • 1,516
  • 4
  • 25
  • 45

7 Answers7

43

It is pretty simple to calculate the zoom level. See the snippet below. You can get the mRect parameter from the visibleMapRect property on your MKMapView instance.

+ (NSUInteger)zoomLevelForMapRect:(MKMapRect)mRect withMapViewSizeInPixels:(CGSize)viewSizeInPixels
{
    NSUInteger zoomLevel = MAXIMUM_ZOOM; // MAXIMUM_ZOOM is 20 with MapKit
    MKZoomScale zoomScale = mRect.size.width / viewSizeInPixels.width; //MKZoomScale is just a CGFloat typedef
    double zoomExponent = log2(zoomScale);
    zoomLevel = (NSUInteger)(MAXIMUM_ZOOM - ceil(zoomExponent));
    return zoomLevel;
}

You could probably just stop at the step for calculating the zoomScale as that will tell you if the zoom has changed at all.

I figured this stuff out from reading Troy Brants excellent blog posts on the topic:

http://troybrant.net/blog/2010/01/mkmapview-and-zoom-levels-a-visual-guide/

Swift 3

extension MKMapView {

    var zoomLevel: Int {
        let maxZoom: Double = 20
        let zoomScale = self.visibleMapRect.size.width / Double(self.frame.size.width)
        let zoomExponent = log2(zoomScale)
        return Int(maxZoom - ceil(zoomExponent))
    }

}
Luca Davanzo
  • 21,000
  • 15
  • 120
  • 146
Aaron
  • 2,403
  • 1
  • 24
  • 25
  • 2
    Wow! this looks like exactly what I need. To sharpen things a bit - Where do I get viewSizeInPixels from? – Rizon Dec 06 '10 at 13:02
  • 5
    viewSizeInPixels is the bounds.size property of your MKMapView instance – Aaron Dec 07 '10 at 06:43
  • Quite helpful, needed to determine if the map is zoomed in to the max. Thanks. – Wolfram Mar 23 '11 at 10:15
  • Hi, please can you tell me how should i call this method you post please, it requires parameters, what value should i put when i call it, thx in advance ;) – Malloc Apr 10 '11 at 20:27
  • 1
    In case it isn't immediately obvious you can get the `mapRect` from `visibleMapRect` property of the MKMapView instance. – Gerry Shaw Dec 21 '13 at 07:43
  • 1
    I have tried to use your code but found some strange thing. I have made some debugging and I'm confused now. `MKZoomScale zoomScale` returns me something like 4727 but when I get zoom in `- (BOOL)canDrawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale` it is about 0.000488.... Where am I wrong? – mike-dutka Mar 10 '15 at 17:38
11

I found this very helpful and developed the following code based on these answers.

- (void)mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated 
{
   mapRegion = self.mapView.region;
}


-(void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated{

MKCoordinateRegion newRegion = self.mapView.region;

NSInteger zFactor;
if ((mapRegion.span.latitudeDelta/newRegion.span.latitudeDelta) > 1.5){
    NSLog(@"Zoom in changed");
    zFactor = 20;
    CustomPlacemark *aO; 
    MKAnnotationView *aV; 
    for (aO in self.mapView.annotations) {
        aV = [[self mapView] viewForAnnotation:aO];
        aV.frame = CGRectMake(aV.frame.origin.x, aV.frame.origin.y, aV.frame.size.width+zFactor, aV.frame.size.height+zFactor);
        [[[self mapView] viewForAnnotation:aO] setFrame:aV.frame];
    }
}
if ((mapRegion.span.latitudeDelta/newRegion.span.latitudeDelta) < 0.75){
    NSLog(@"Zoom out");
    zFactor = -20;
    CustomPlacemark *aO; 
    MKAnnotationView *aV; 
    for (aO in self.mapView.annotations) {
        aV = [[self mapView] viewForAnnotation:aO];
        aV.frame = CGRectMake(aV.frame.origin.x, aV.frame.origin.y, aV.frame.size.width+zFactor, aV.frame.size.height+zFactor);
        [[[self mapView] viewForAnnotation:aO] setFrame:aV.frame];
    }
  }
}
Nagarjun
  • 6,557
  • 5
  • 33
  • 51
Brian Oldfield
  • 111
  • 1
  • 2
  • My initial attempt was 90% of this solution, the span ratio is what I was missing (I was expecting a simple move to keep the same lat/lon span range, I was incorrect) – Lytic Jan 18 '17 at 19:06
4

Much more simpler answer:

The easiest way to get an Integer of the current zoom level, is by using the MapView function: regionDidChangeAnimated. This function recognizes every change in zoom and will give you the basis for the calculation of the zoom factor.

Just insert this function into your MapView class (works for Swift 3.0):

var mapView: MKMapView! = nil

...

func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
    let zoomWidth = mapView.visibleMapRect.size.width
    let zoomFactor = Int(log2(zoomWidth)) - 9
    print("...REGION DID CHANGE: ZOOM FACTOR \(zoomFactor)")
}

And you will get a zoomFactor value out of it, where 0 is the most near point you can zoom into the map and every higher value is a far far away zoom... :-)

MosTwanTedT
  • 323
  • 3
  • 12
  • Yup this is called whenever the map actually changes which is when I car about the zoom. Thanks! – Ryan R Jan 23 '18 at 04:30
2

I have the following MKMapView category in which I include a method for quickly getting the current zoom level for the map:

@implementation MKMapView (ZoomLevel)

- (NSUInteger) zoomLevel {
    MKCoordinateRegion region = self.region;

    double centerPixelX = [MKMapView longitudeToPixelSpaceX: region.center.longitude];
    double topLeftPixelX = [MKMapView longitudeToPixelSpaceX: region.center.longitude - region.span.longitudeDelta / 2];

    double scaledMapWidth = (centerPixelX - topLeftPixelX) * 2;
    CGSize mapSizeInPixels = self.bounds.size;
    double zoomScale = scaledMapWidth / mapSizeInPixels.width;
    double zoomExponent = log(zoomScale) / log(2);
    double zoomLevel = 21 - zoomExponent;

    return zoomLevel;
}

@end

To obtain the zoom level, you can call the following in your delegates and determine if the zoom level has changed:

- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
    NSUInteger zoomLevel = [mapView zoomLevel];
}
u10int
  • 219
  • 2
  • 8
  • This is wonderful except -longitudeToPixelSpaceX: is a big unknown. – SG1 Nov 27 '12 at 01:14
  • 1
    MKMapRect visibleRect = self.visibleMapRect; double centerPixelX = visibleRect.origin.x+visibleRect.size.width/2; double topLeftPixelX = visibleRect.origin.x; – SG1 Nov 27 '12 at 01:36
  • 2
    I have recently simplified this in my own apps, which works well and is more concise https://gist.github.com/4178336 – u10int Nov 30 '12 at 20:21
1

you can listen to the mapView:regionDidChangeAnimated: method. However, this doesn't tell you if the zoom level changed, just if the map was animated.

You will also need to listen to the region property of the map view. This contains the latitudeDelta and the longitudeDelta values which can be used to calculate if the zoom level has changed.

i.e. in the .h file

@class MyMapViewController {
    ...
    MKCoordinateRegion mapRegion;
    }
@end

and in your .m file

- (void)mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated {
    mapRegion = mapView.region;
}

- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
    newRegion = mapView.region;

    if (mapRegion.span.latitudeDelta != newRegion.span.latitudeDelta ||
        mapRegion.span.longitudeDelta != newRegion.span.longitudeDelta)
        NSLog(@"The zoom has changed");
}

This should detect if the map zoom has changed.

however, you should wach out for the zoom changing because the earth is curved :( If the map is scrolled the latitudeDelta and longitudeDelta will change slightly just because of the shape of the Earth, not because the user has zoomed. You might have to detect a large change in the deltas and ignore slight changes.

Hope that helps.

deanWombourne
  • 38,189
  • 13
  • 98
  • 110
  • Much more complete answer than mine. I think the longitude delta changes as you go towards the poles, but the latitude delta stays constant wherever you go. Might need some testing to check that it's constant enough to use. – nevan king Dec 05 '10 at 15:53
  • I can never remember which changes and which doesn't so I've just tested both in my answer :) I suppose you could test to see if they bost change it must be a user zoom, if only one changes it much be a scroll? – deanWombourne Dec 05 '10 at 16:19
  • Guys I wish you could just had a slight check to your "solutions" before publishing them. Non of your ideas work. The span delta does change - and change a lot. – Rizon Dec 06 '10 at 00:36
  • 5 minutes testing tells me that latitudeDelta changes all the time, longitudeDelta changes only when you zoom so my answer will work if you only test the longitudeDelta. And we're not here to solve it for you, we're here to help you find the solution yourself, which I think we did. – deanWombourne Dec 06 '10 at 14:06
  • no problem and tbh, I should have put in my answer that it was untested :) – deanWombourne Dec 08 '10 at 13:55
0

Count zoom scale in MKMapView - Swift solution

I created following extension for MKMapView, so you can get a scale of zoom on map. The solution is similar as presented above but in Swift.

There is also additional function scaleWithPrecision(_:Int64) for rounding that scale what allow to filter out f.ex. little zoom changes on MapView

extension MKMapView {

    var scale: Double {

        return self.scaleWithPrecision(1)
    }

    func scaleWithPrecision(precision: Int64) -> Double {

        let mapBoundsWidth = Double(self.bounds.size.width)
        let mapRectWidth:Double = self.visibleMapRect.size.width

        let scale: Double = round(Double(precision)*mapBoundsWidth/mapRectWidth)/Double(precision)

        return scale
    }
}
lukszar
  • 1,252
  • 10
  • 13
-1

You could save a latitude delta, then when regionDidChangeAnimated: is called, check to see if the new latitude delta is different. I think the latitude delta stays constant as long as the map isn't zoomed.

nevan king
  • 112,709
  • 45
  • 203
  • 241