开发者

Restrict MKMapView scrolling

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 开发者_JAVA百科suggestions?

Thanks, Matt


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;
}


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);

}


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)
            }
        }
 }


MapKit now does this natively in iOS 13

You can explicitly set a boundary to restrict panning.

let boundaryRegion = MKCoordinateRegion(...) // the region you want to restrict
let cameraBoundary = CameraBoundary(region: boundaryRegion)
mapView.setCameraBoundary(cameraBoundary: cameraBoundary, animated: true)

See WWDC 2019 video at 2378 seconds for a demonstration.

You can also restrict zoom levels

let zoomRange = CameraZoomRange(minCenterCoordinateDistance: 100,
    maxCenterCoordinateDistance: 500)
mapView.setCameraZoomRange(zoomRange, animated: true)

References

  • MapKit documentation on "Constraining the Map View"
  • class reference for MKMapView.CameraBoundary
  • class reference for MKMapView.CameraZoomRange


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)
}


SWIFT 5

simple solution for use inside mapViewDidFinishLoadingMap:

    func mapViewDidFinishLoadingMap(_ mapView: MKMapView) {

        //center of USA, roughly. for example
        let center = CLLocationCoordinate2D(latitude: 38.573936, longitude: -92.603760) 
        let latMeters = CLLocationDistance(10_000_000.00) //left and right pan
        let longMeters = CLLocationDistance(5_000_000.00) //up and down pan
        
        let coordinateRegion = MKCoordinateRegion(
            center: center,
            latitudinalMeters: latMeters,
            longitudinalMeters: longMeters)
        
        let cameraBoundary = MKMapView.CameraBoundary(coordinateRegion: coordinateRegion)
        mapView.setCameraBoundary(cameraBoundary, animated: true)
    }
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜