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)
}
精彩评论