Restrict MKMapView scrolling

2019-01-07 10:06发布

问题:

I'm trying to add a custom image to an MKMapView as an MKOverlayView - I need to restrict users from being able to scroll outside the bounds of the overlay. Are there any existing functions to do this? Or any other suggestions?

Thanks, Matt

回答1:

If you just want to freeze the map view at the overlay, you could set the map view's region to the overlay's bounds and set scrollEnabled and zoomEnabled to NO.

But that won't let the user scroll or zoom inside the overlay's bounds.

There aren't built-in ways to restrict the map view to the overlay's bounds so you'd have to do it manually. First, make sure your MKOverlay object implements the boundingMapRect property. That can then be used in the regionDidChangeAnimated delegate method to manually adjust the view as needed.

Here's an example of how this could be done.
Code below should be in the class that has the MKMapView.
Make sure the map view is initially set to a region where the overlay is visible.

//add two ivars to the .h...
MKMapRect lastGoodMapRect;
BOOL manuallyChangingMapRect;

//in the .m...
- (void)mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated
{
    if (manuallyChangingMapRect)
        return;     
    lastGoodMapRect = mapView.visibleMapRect;
}

- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated
{
    if (manuallyChangingMapRect) //prevents possible infinite recursion when we call setVisibleMapRect below
        return;     

    // "theOverlay" below is a reference to your MKOverlay object.
    // It could be an ivar or obtained from mapView.overlays array.

    BOOL mapContainsOverlay = MKMapRectContainsRect(mapView.visibleMapRect, theOverlay.boundingMapRect);

    if (mapContainsOverlay)
    {
        // The overlay is entirely inside the map view but adjust if user is zoomed out too much...
        double widthRatio = theOverlay.boundingMapRect.size.width / mapView.visibleMapRect.size.width;
        double heightRatio = theOverlay.boundingMapRect.size.height / mapView.visibleMapRect.size.height;
        if ((widthRatio < 0.6) || (heightRatio < 0.6)) //adjust ratios as needed
        {
            manuallyChangingMapRect = YES;
            [mapView setVisibleMapRect:theOverlay.boundingMapRect animated:YES];
            manuallyChangingMapRect = NO;
        }
    }
    else
        if (![theOverlay intersectsMapRect:mapView.visibleMapRect])
        {
            // Overlay is no longer visible in the map view.
            // Reset to last "good" map rect...
            [mapView setVisibleMapRect:lastGoodMapRect animated:YES];
        }   
}

I tried this with the built-in MKCircle overlay and seems to work well.


EDIT:

It does work well 95% of the time, however, I have confirmed through some testing that it might oscillate between two locations, then enter an infinite loop. So, I edited it a bit, I think this should solve the problem:

// You can safely delete this method:
- (void)mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated {

}

- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
     // prevents possible infinite recursion when we call setVisibleMapRect below
    if (manuallyChangingMapRect) {
        return;
    }

    // "theOverlay" below is a reference to your MKOverlay object.
    // It could be an ivar or obtained from mapView.overlays array.

    BOOL mapContainsOverlay = MKMapRectContainsRect(mapView.visibleMapRect, theOverlay.boundingMapRect);

    if (mapContainsOverlay) {
        // The overlay is entirely inside the map view but adjust if user is zoomed out too much...
        double widthRatio = theOverlay.boundingMapRect.size.width / mapView.visibleMapRect.size.width;
        double heightRatio = theOverlay.boundingMapRect.size.height / mapView.visibleMapRect.size.height;
        // adjust ratios as needed
        if ((widthRatio < 0.6) || (heightRatio < 0.6)) {
            manuallyChangingMapRect = YES;
            [mapView setVisibleMapRect:theOverlay.boundingMapRect animated:YES];
            manuallyChangingMapRect = NO;
        }
    } else if (![theOverlay intersectsMapRect:mapView.visibleMapRect]) {
        // Overlay is no longer visible in the map view.
        // Reset to last "good" map rect...
        manuallyChangingMapRect = YES;
        [mapView setVisibleMapRect:lastGoodMapRect animated:YES];
        manuallyChangingMapRect = NO;
    } else {
        lastGoodMapRect = mapView.visibleMapRect;
    }
}

And just in case someone is looking for a quick MKOverlay solution, here is one:

- (void)viewDidLoad {
    [super viewDidLoad];

    MKCircle* circleOverlay = [MKCircle circleWithMapRect:istanbulRect];
    [_mapView addOverlay:circleOverlay];

    theOverlay = circleOverlay;
}

- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id<MKOverlay>)overlay {
    MKCircleView* circleOverlay = [[MKCircleView alloc] initWithCircle:overlay];
    [circleOverlay setStrokeColor:[UIColor mainColor]];
    [circleOverlay setLineWidth:4.f];

    return circleOverlay;
}


回答2:

In my case, I needed to restrict bounds to tiled overlay which has an upperleft / lowerRight coordinates. Code above still works well, but substituted theOverlay.boundingMapRect for MKMapRect paddedBoundingMapRect

- (void)mapView:(MKMapView *)_mapView regionDidChangeAnimated:(BOOL)animated
{
if (manuallyChangingMapRect) //prevents possible infinite recursion when we call setVisibleMapRect below
    return;     

[self updateDynamicPaddedBounds];

MKMapPoint pt =  MKMapPointForCoordinate( mapView.centerCoordinate);

BOOL mapInsidePaddedBoundingRect = MKMapRectContainsPoint(paddedBoundingMapRect,pt );

if (!mapInsidePaddedBoundingRect)
{
    // Overlay is no longer visible in the map view.
    // Reset to last "good" map rect...

    manuallyChangingMapRect = YES;
    [mapView setVisibleMapRect:lastGoodMapRect animated:YES];
    manuallyChangingMapRect = NO;


}


-(void)updateDynamicPaddedBounds{

ENTER_METHOD;

CLLocationCoordinate2D  northWestPoint= CLLocationCoordinate2DMake(-33.841171,151.237318 );
CLLocationCoordinate2D  southEastPoint= CLLocationCoordinate2DMake(-33.846127, 151.245058);



MKMapPoint upperLeft = MKMapPointForCoordinate(northWestPoint);
MKMapPoint lowerRight = MKMapPointForCoordinate(southEastPoint);
double width = lowerRight.x - upperLeft.x;
double height = lowerRight.y - upperLeft.y;


MKMapRect mRect = mapView.visibleMapRect;
MKMapPoint eastMapPoint = MKMapPointMake(MKMapRectGetMinX(mRect), MKMapRectGetMidY(mRect));
MKMapPoint westMapPoint = MKMapPointMake(MKMapRectGetMaxX(mRect), MKMapRectGetMidY(mRect));
MKMapPoint northMapPoint = MKMapPointMake(MKMapRectGetMidX(mRect), MKMapRectGetMaxY(mRect));
MKMapPoint southMapPoint = MKMapPointMake(MKMapRectGetMidX(mRect), MKMapRectGetMinY(mRect));

double xMidDist = abs(eastMapPoint.x -  westMapPoint.x)/2;
double yMidDist = abs(northMapPoint.y -  southMapPoint.y)/2;


upperLeft.x = upperLeft.x + xMidDist;
upperLeft.y = upperLeft.y + yMidDist;


double paddedWidth =  width - (xMidDist*2); 
double paddedHeight = height - (yMidDist*2);

paddedBoundingMapRect= MKMapRectMake(upperLeft.x, upperLeft.y, paddedWidth, paddedHeight);

}



回答3:

Anna's (https://stackoverflow.com/a/4126011/3191130) solution in Swift 3.0, I added to an extension:

extension HomeViewController: MKMapViewDelegate {
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
    if manuallyChangingMapRect {
        return
    }
    guard let overlay = self.mapOverlay else {
        print("Overlay is nil")
        return
    }
    guard let lastMapRect = self.lastGoodMapRect else {
        print("LastGoodMapRect is nil")
        return
    }

    let mapContainsOverlay = MKMapRectContainsRect(mapView.visibleMapRect, overlay.boundingMapRect)
    if mapContainsOverlay {
        let widthRatio: Double = overlay.boundingMapRect.size.width / mapView.visibleMapRect.size.width
        let heightRatio: Double = overlay.boundingMapRect.size.height / mapView.visibleMapRect.size.height
        // adjust ratios as needed
        if (widthRatio < 0.9) || (heightRatio < 0.9) {
            manuallyChangingMapRect = true
            mapView.setVisibleMapRect(overlay.boundingMapRect, animated: true)
            manuallyChangingMapRect = false
        }
    } else if !overlay.intersects(mapView.visibleMapRect) {
            // Overlay is no longer visible in the map view.
            // Reset to last "good" map rect...
            manuallyChangingMapRect = true
            mapView.setVisibleMapRect(lastMapRect, animated: true)
            manuallyChangingMapRect = false
        }
        else {
            lastGoodMapRect = mapView.visibleMapRect
    }
}
}

To setup the map use this:

override func viewDidLoad() {
    super.viewDidLoad()
    setupMap()
}

func setupMap() {
    mapView.delegate = self
    let radius:CLLocationDistance = 1000000
    mapOverlay = MKCircle(center: getCenterCoord(), radius: radius)
    if let mapOverlay = mapOverlay  {
        mapView.add(mapOverlay)
    }
    mapView.setRegion(MKCoordinateRegionMake(getCenterCoord(), getSpan()), animated: true)
    lastGoodMapRect = mapView.visibleMapRect
}

func getCenterCoord() -> CLLocationCoordinate2D {
    return CLLocationCoordinate2DMake(LAT, LON)
}
func getSpan() -> MKCoordinateSpan {
    return MKCoordinateSpanMake(10, 10)
}


回答4:

A Good Answer for Swift 4

with following code you can detect bound limit for scroll

NOTE: in following code 5000 number is amount of restriced area in terms of meters. so you can use like this > let restricedAreaMeters = 5000

func detectBoundingBox(location: CLLocation) {
        let latRadian = degreesToRadians(degrees: CGFloat(location.coordinate.latitude))
        let degLatKm = 110.574235
        let degLongKm = 110.572833 * cos(latRadian)
        let deltaLat = 5000 / 1000.0 / degLatKm 
        let deltaLong = 5000 / 1000.0 / degLongKm

        southLimitation = location.coordinate.latitude - deltaLat
        westLimitation = Double(CGFloat(location.coordinate.longitude) - deltaLong)
        northLimitation =  location.coordinate.latitude + deltaLat
        eastLimitation = Double(CGFloat(location.coordinate.longitude) + deltaLong)
    }

    func degreesToRadians(degrees: CGFloat) -> CGFloat {
        return degrees * CGFloat(M_PI) / 180
    }

and finally with overrided method at bellow if user got out from bounded area will be returned to latest allowed coordinate.

 var lastCenterCoordinate: CLLocationCoordinate2D!
 extension UIViewController: MKMapViewDelegate {
        func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { 
           let coordinate = CLLocationCoordinate2DMake(mapView.region.center.latitude, mapView.region.center.longitude) 
           let latitude = mapView.region.center.latitude
           let longitude = mapView.region.center.longitude

            if latitude < northLimitation && latitude > southLimitation && longitude < eastLimitation && longitude > westLimitation {
                lastCenterCoordinate = coordinate
            } else {
                span = MKCoordinateSpanMake(0, 360 / pow(2, Double(16)) * Double(mapView.frame.size.width) / 256)
                let region = MKCoordinateRegionMake(lastCenterCoordinate, span)
                mapView.setRegion(region, animated: true)
            }
        }
 }