4

I'm trying to set a minimum zoom level on my map in Swift 2. I can't find any documentation on how to restrict a map from being zoomed too far in. What I've decided to try is to monitor for map movement (such as drag or zoom) and then set MKZoomScaleback to a minimum.

Most of the answers I've found for regionDidChangeAnimated are in Objective C, which I don't know and I'm having trouble converting them to Swift.

I tried implementing @hEADcRASH's answer: https://stackoverflow.com/a/30924768/4106552, but it doesn't trigger and print anything to the console when the map is moved in the simulator.

Can anyone tell me what I'm doing wrong? I'm new to Swift, so it could be a small error. Also, let me know if there is a lightweight way to solve for restricting the zoom level on a map. I'm worried that the monitor for movement will slow down the map animation a bit. Thanks for the help.

Here is my view controller. import UIKit import Parse import MapKit

class SearchRadiusViewController: UIViewController, CLLocationManagerDelegate, MKMapViewDelegate {

@IBOutlet weak var map: MKMapView!

@IBOutlet weak var menuBtn: UIBarButtonItem!

var locationManager = CLLocationManager()

override func viewDidLoad() {
    super.viewDidLoad()

    //menu button control
    if self.revealViewController() != nil {
        menuBtn.target = self.revealViewController()
        menuBtn.action = "revealToggle:"
        self.view.addGestureRecognizer(self.revealViewController().panGestureRecognizer())
    }

    //user location
    locationManager.delegate = self
    locationManager.desiredAccuracy = kCLLocationAccuracyBest
    locationManager.requestWhenInUseAuthorization()
    locationManager.startUpdatingLocation()


}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
}


func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {

    //set map
    let location:CLLocationCoordinate2D = manager.location!.coordinate
    let latitude = location.latitude
    let longitude = location.longitude
    let latDelta:CLLocationDegrees = 0.1
    let longDelta:CLLocationDegrees = 0.1
    let span:MKCoordinateSpan = MKCoordinateSpanMake(latDelta, longDelta)
    let maplocation:CLLocationCoordinate2D = CLLocationCoordinate2DMake(latitude, longitude)
    let region:MKCoordinateRegion = MKCoordinateRegionMake(maplocation, span)
    map.setRegion(region, animated: true)

    //stop updating location, only need user location once to position map.
    manager.stopUpdatingLocation()

}

//Attempt to monitor for map movement based on hEADcRASH's answer.
private var mapChangedFromUserInteraction = false

private func mapViewRegionDidChangeFromUserInteraction() -> Bool {
    let view = self.map.subviews[0]
    //  Look through gesture recognizers to determine whether this region change is from user interaction
    if let gestureRecognizers = view.gestureRecognizers {
        for recognizer in gestureRecognizers {
            if( recognizer.state == UIGestureRecognizerState.Began || recognizer.state == UIGestureRecognizerState.Ended ) {
                return true
            }
        }
    }
    return false
}

func mapView(mapView: MKMapView, regionWillChangeAnimated animated: Bool) {
    print("yes")
    mapChangedFromUserInteraction = mapViewRegionDidChangeFromUserInteraction()
    if (mapChangedFromUserInteraction) {
        // user changed map region
        print("user changed map in WILL")
    }
}

func mapView(mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
            print("yes ddd")
    if (mapChangedFromUserInteraction) {
        // user changed map region
        print("user changed map in Did")
    }
}
}
Community
  • 1
  • 1
tylerSF
  • 1,479
  • 2
  • 16
  • 25
  • Are you setting the delegate of the mapView in the storyboard? If not you should add self.map.delegate = self in the viewDidLoad method. – LorenzOliveto Jan 13 '16 at 16:51
  • thanks @lorenzoliveto, adding `self.map.delegate = self` to the viewDidLoad is working. mapViewRegionDidChangeAnimated is working! Do you have an opinion the best way to set a max zoom level? Should I go the route of watching for map movement/zoom and resetting the map zoom? – tylerSF Jan 14 '16 at 03:11
  • Yes, I think that this is the only option that you have. regionWillChangeAnimated: is called multiple times during the scrolling and regionDidChangeAnimated: is called only one time after the scroll. Try adding the reset code in both and see how the performances are affected. – LorenzOliveto Jan 14 '16 at 07:39

2 Answers2

6

After reviewing and combining a number of other questions/answers and the help of @lorenzoliveto I've got it working in Swift. Please leave a comment if there a better/more lightweight way to achieve the same thing.

I added self.map.delegate = self to the viewDidLoad function.

Below is the code for how I'm monitoring for map movement and then once a user has zoomed in "too far" and the width of the map goes below 2 miles I then zoom out the map using mapView.setRegion.

private var mapChangedFromUserInteraction = false

private func mapViewRegionDidChangeFromUserInteraction() -> Bool {
    let view = self.map.subviews[0]
    //  Look through gesture recognizers to determine whether this region change is from user interaction
    if let gestureRecognizers = view.gestureRecognizers {
        for recognizer in gestureRecognizers {
            if( recognizer.state == UIGestureRecognizerState.Began || recognizer.state == UIGestureRecognizerState.Ended ) {
                return true
            }
        }
    }
    return false
}

func mapView(mapView: MKMapView, regionWillChangeAnimated animated: Bool) {
    mapChangedFromUserInteraction = mapViewRegionDidChangeFromUserInteraction()
    if (mapChangedFromUserInteraction) {
        // user will change map region
        print("user WILL change map.")

        // calculate the width of the map in miles.
        let mRect: MKMapRect = mapView.visibleMapRect
        let eastMapPoint = MKMapPointMake(MKMapRectGetMinX(mRect), MKMapRectGetMidY(mRect))
        let westMapPoint = MKMapPointMake(MKMapRectGetMaxX(mRect), MKMapRectGetMidY(mRect))
        let currentDistWideInMeters = MKMetersBetweenMapPoints(eastMapPoint, westMapPoint)
        let milesWide = currentDistWideInMeters / 1609.34  // number of meters in a mile
        print(milesWide)
        print("^miles wide")

        // check if user zoomed in too far and zoom them out.
        if milesWide < 2.0 {
            var region:MKCoordinateRegion = mapView.region
            var span:MKCoordinateSpan = mapView.region.span
            span.latitudeDelta = 0.04
            span.longitudeDelta = 0.04
            region.span = span;
            mapView.setRegion(region, animated: true)
            print("map zoomed back out")
        }

    }
}

func mapView(mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
    if (mapChangedFromUserInteraction) {
        // user changed map region
        print("user CHANGED map.")
        print(mapView.region.span.latitudeDelta)
        print(mapView.region.span.longitudeDelta)

        // calculate the width of the map in miles.
        let mRect: MKMapRect = mapView.visibleMapRect
        let eastMapPoint = MKMapPointMake(MKMapRectGetMinX(mRect), MKMapRectGetMidY(mRect))
        let westMapPoint = MKMapPointMake(MKMapRectGetMaxX(mRect), MKMapRectGetMidY(mRect))
        let currentDistWideInMeters = MKMetersBetweenMapPoints(eastMapPoint, westMapPoint)
        let milesWide = currentDistWideInMeters / 1609.34  // number of meters in a mile
        print(milesWide)
        print("^miles wide")

        // check if user zoomed in too far and zoom them out.
        if milesWide < 2.0 {
            var region:MKCoordinateRegion = mapView.region
            var span:MKCoordinateSpan = mapView.region.span
            span.latitudeDelta = 0.04
            span.longitudeDelta = 0.04
            region.span = span;
            mapView.setRegion(region, animated: true)
            print("map zoomed back out")
        }
    }

UPDATE: 3/7, I discovered an interesting bug in the implementation above. On the simulator it works fine when clicking to zoom, but when you use the pinch to zoom (option + click) the simulator stops allowing you to drag the map around after it animates the zoom back out. This also happened on the beta version on my iphone. I added dispatch_async around the blocks that animate that map back to their position and it appears to be working on the simulator. It no longer appears frozen after it animates and I can continue to drag around the map and try to zoom in.

dispatch_async(dispatch_get_main_queue(), {
   var region:MKCoordinateRegion = mapView.region
   var span:MKCoordinateSpan = mapView.region.span
   span.latitudeDelta = 0.04
   span.longitudeDelta = 0.04
   region.span = span;
   mapView.setRegion(region, animated: true)
   print("map zoomed back out")
})
tylerSF
  • 1,479
  • 2
  • 16
  • 25
  • 2
    I was looking for how to detect map changed and found this. Seems that I only need the `func mapViewRegionDidChangeFromUserInteraction()` thanks for sharing! – Dan Apr 16 '16 at 23:42
0

The solution that works for me is one where I set the zoom range. This approach may not have been available at the time the question was asked. The code fragment below is what I use. I'm not entirely sure what the distance units are, but I believe they are meters. Figuring out what range works in a given instance may be a matter of trial and error.

        let mapView = MKMapView(frame: .zero)        
        let zoomRange = MKMapView.CameraZoomRange(
            minCenterCoordinateDistance: 120000,
            maxCenterCoordinateDistance: 1600000
        )
        mapView.cameraZoomRange = zoomRange
John Morris
  • 396
  • 2
  • 10