Restrict MKMapView scrolling

后端 未结 6 1526
独厮守ぢ
独厮守ぢ 2020-11-29 03:28

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 th

相关标签:
6条回答
  • 2020-11-29 04:17

    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 讨论(0)
  • 2020-11-29 04:19

    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
    0 讨论(0)
  • 2020-11-29 04:22

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

    }

    0 讨论(0)
  • 2020-11-29 04:25

    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;
    }
    
    0 讨论(0)
  • 2020-11-29 04:31

    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)
    }
    
    0 讨论(0)
  • 2020-11-29 04:32

    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)
                }
            }
     }
    
    0 讨论(0)
提交回复
热议问题