【问题标题】:Is there way to limit MKMapView maximum zoom level?有没有办法限制 MKMapView 最大缩放级别?
【发布时间】:2025-12-04 09:30:01
【问题描述】:

问题是 - 有没有办法限制 MKMapView 的最大缩放级别?或者有没有办法在用户缩放到没有可用地图图像的级别时进行跟踪?

【问题讨论】:

    标签: iphone mkmapview zooming


    【解决方案1】:

    如果您仅使用 iOS 7+,则可以获取/设置一个新的 camera.altitude 属性以强制缩放级别。它相当于azdev的解决方案,但不需要外部代码。

    在测试中,我还发现如果你反复尝试放大细节可能会进入无限循环,所以我在下面的代码中设置了一个 var 来防止这种情况。

    - (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
        // enforce maximum zoom level
        if (_mapView.camera.altitude < 120.00 && !_modifyingMap) {
            _modifyingMap = YES; // prevents strange infinite loop case
    
            _mapView.camera.altitude = 120.00;
    
            _modifyingMap = NO;
        }
    }
    

    【讨论】:

    • 你不能每次低于 120 时也将高度设置为 125 吗?
    • @EricWelander 当然。但这会改变我的答案吗?无论是 120 还是 125,_mapView.camera.altitude
    • 可能会消除对布尔标志的需求?
    • 将新高度设置为比 if 语句中更高的数字确实消除了我对布尔标志的需要。
    • 我认为这将是一个适合我的解决方案,但事实并非如此。当我查找位置并添加注释时,我需要进行检查。添加注释后,我调用了 showAnnotations(),它改变了缩放(如果只有一个注释,则太近了)。在此之前设置高度没有任何作用,因为 showAnnotations() 会重置缩放。之后设置高度不起作用,因为新添加的注释由于某种原因没有出现。
    【解决方案2】:

    您可以使用mapView:regionWillChangeAnimated: 委托方法来侦听区域更改事件,如果该区域比您的最大区域宽,则使用setRegion:animated: 将其设置回最大区域,以向您的用户表明他们可以' t 缩小那么远。方法如下:

    - (void)mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated
    - (void)setRegion:(MKCoordinateRegion)region animated:(BOOL)animated
    

    【讨论】:

    • 谢谢。对我来说似乎没问题,只是似乎在 regionDidChangeAnimated 方法而不是 regionDidChangeAnimated 方法中这样做更好
    • 很高兴它成功了。将方法更改为regionDidChangeAnimated 有什么不同?是时间问题吗?
    • 你能提供你在 regionDidAnimate 中使用的代码吗?每次我从 regionDidAnimate 内部调用 [mapView setRegion] 时,我都会进入一个无限循环。
    • 你好抽认卡。针对您的问题提出一个新问题可能会更容易。您可以通过代码示例更全面地解释它。
    • 我遇到了和抽认卡一样的问题。我认为这个问题没有得到充分的回答——有人可以发布一些不会创建无限循环的工作源代码吗?
    【解决方案3】:

    我刚刚花了一些时间为我正在构建的应用程序工作。这是我想出的:

    1. 我从 Troy Brant 的 this page 脚本开始,我认为这是设置地图视图的更好方法。

    2. 我添加了一个返回当前缩放级别的方法。

      在MKMapView+ZoomLevel.h中:

      - (double)getZoomLevel;
      

      在MKMapView+ZoomLevel.m中:

      // Return the current map zoomLevel equivalent, just like above but in reverse
      - (double)getZoomLevel{
          MKCoordinateRegion reg=self.region; // the current visible region
          MKCoordinateSpan span=reg.span; // the deltas
          CLLocationCoordinate2D centerCoordinate=reg.center; // the center in degrees
          // Get the left and right most lonitudes
          CLLocationDegrees leftLongitude=(centerCoordinate.longitude-(span.longitudeDelta/2));
          CLLocationDegrees rightLongitude=(centerCoordinate.longitude+(span.longitudeDelta/2));
          CGSize mapSizeInPixels = self.bounds.size; // the size of the display window
      
          // Get the left and right side of the screen in fully zoomed-in pixels
          double leftPixel=[self longitudeToPixelSpaceX:leftLongitude]; 
          double rightPixel=[self longitudeToPixelSpaceX:rightLongitude];
          // The span of the screen width in fully zoomed-in pixels
          double pixelDelta=abs(rightPixel-leftPixel);
      
          // The ratio of the pixels to what we're actually showing
          double zoomScale= mapSizeInPixels.width /pixelDelta;
          // Inverse exponent
          double zoomExponent=log2(zoomScale);
          // Adjust our scale
          double zoomLevel=zoomExponent+20; 
          return zoomLevel;
      }
      

      此方法依赖于上面链接代码中的一些私有方法。

    3. 我将此添加到我的 MKMapView 委托中(正如上面@vladimir 推荐的那样)

      - (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
          NSLog(@"%f",[mapView getZoomLevel]);
          if([mapView getZoomLevel]<10) {
              [mapView setCenterCoordinate:[mapView centerCoordinate] zoomLevel:10 animated:TRUE];
          }
      }
      

      如果用户离得太远,这会产生重新缩放的效果。您可以使用 regionWillChangeAnimated 来防止地图“弹回”。

      关于上面的循环cmets,看起来这个方法只迭代一次。

    【讨论】:

    • 此代码在我的应用程序中无法正常工作,我浪费了几个小时,最后通过将其更改为: - (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated { newDesiredZoomLevel = [mapView getZoomLevel]; NSLog(@"%f",newDesiredZoomLevel); if((newDesiredZoomLevel > 2.0f) && (newDesiredZoomLevel
    【解决方案4】:

    是的,这是可行的。首先,使用MKMapView+ZoomLevel扩展MKMapView。

    然后,在您的 MKMapViewDelegate 中实现它:

    - (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated
    {
        // Constrain zoom level to 8.
        if( [mapView zoomLevel] < 8 )
        {
            [mapView setCenterCoordinate:mapView.centerCoordinate 
                zoomLevel:8 
                animated:NO];
        }
    }
    

    【讨论】:

      【解决方案5】:

      这是使用MKMapView+ZoomLevel 和@T.Markle 用 Swift 3 重写的代码答案:

      import Foundation
      import MapKit
      
      fileprivate let MERCATOR_OFFSET: Double = 268435456
      fileprivate let MERCATOR_RADIUS: Double = 85445659.44705395
      
      extension MKMapView {
      
          func getZoomLevel() -> Double {
      
              let reg = self.region
              let span = reg.span
              let centerCoordinate = reg.center
      
              // Get the left and right most lonitudes
              let leftLongitude = centerCoordinate.longitude - (span.longitudeDelta / 2)
              let rightLongitude = centerCoordinate.longitude + (span.longitudeDelta / 2)
              let mapSizeInPixels = self.bounds.size
      
              // Get the left and right side of the screen in fully zoomed-in pixels
              let leftPixel = self.longitudeToPixelSpaceX(longitude: leftLongitude)
              let rightPixel = self.longitudeToPixelSpaceX(longitude: rightLongitude)
              let pixelDelta = abs(rightPixel - leftPixel)
      
              let zoomScale = Double(mapSizeInPixels.width) / pixelDelta
              let zoomExponent = log2(zoomScale)
              let zoomLevel = zoomExponent + 20
      
              return zoomLevel
          }
      
          func setCenter(coordinate: CLLocationCoordinate2D, zoomLevel: Int, animated: Bool) {
      
              let zoom = min(zoomLevel, 28)
      
              let span = self.coordinateSpan(centerCoordinate: coordinate, zoomLevel: zoom)
              let region = MKCoordinateRegion(center: coordinate, span: span)
      
              self.setRegion(region, animated: true)
          }
      
          // MARK: - Private func
      
          private func coordinateSpan(centerCoordinate: CLLocationCoordinate2D, zoomLevel: Int) -> MKCoordinateSpan {
      
              // Convert center coordiate to pixel space
              let centerPixelX = self.longitudeToPixelSpaceX(longitude: centerCoordinate.longitude)
              let centerPixelY = self.latitudeToPixelSpaceY(latitude: centerCoordinate.latitude)
      
              // Determine the scale value from the zoom level
              let zoomExponent = 20 - zoomLevel
              let zoomScale = NSDecimalNumber(decimal: pow(2, zoomExponent)).doubleValue
      
              // Scale the map’s size in pixel space
              let mapSizeInPixels = self.bounds.size
              let scaledMapWidth = Double(mapSizeInPixels.width) * zoomScale
              let scaledMapHeight = Double(mapSizeInPixels.height) * zoomScale
      
              // Figure out the position of the top-left pixel
              let topLeftPixelX = centerPixelX - (scaledMapWidth / 2)
              let topLeftPixelY = centerPixelY - (scaledMapHeight / 2)
      
              // Find delta between left and right longitudes
              let minLng: CLLocationDegrees = self.pixelSpaceXToLongitude(pixelX: topLeftPixelX)
              let maxLng: CLLocationDegrees = self.pixelSpaceXToLongitude(pixelX: topLeftPixelX + scaledMapWidth)
              let longitudeDelta: CLLocationDegrees = maxLng - minLng
      
              // Find delta between top and bottom latitudes
              let minLat: CLLocationDegrees = self.pixelSpaceYToLatitude(pixelY: topLeftPixelY)
              let maxLat: CLLocationDegrees = self.pixelSpaceYToLatitude(pixelY: topLeftPixelY + scaledMapHeight)
              let latitudeDelta: CLLocationDegrees = -1 * (maxLat - minLat)
      
              return MKCoordinateSpan(latitudeDelta: latitudeDelta, longitudeDelta: longitudeDelta)
          }
      
          private func longitudeToPixelSpaceX(longitude: Double) -> Double {
              return round(MERCATOR_OFFSET + MERCATOR_RADIUS * longitude * M_PI / 180.0)
          }
      
          private func latitudeToPixelSpaceY(latitude: Double) -> Double {
              if latitude == 90.0 {
                  return 0
              } else if latitude == -90.0 {
                  return MERCATOR_OFFSET * 2
              } else {
                  return round(MERCATOR_OFFSET - MERCATOR_RADIUS * Double(logf((1 + sinf(Float(latitude * M_PI) / 180.0)) / (1 - sinf(Float(latitude * M_PI) / 180.0))) / 2.0))
              }
          }
      
          private func pixelSpaceXToLongitude(pixelX: Double) -> Double {
              return ((round(pixelX) - MERCATOR_OFFSET) / MERCATOR_RADIUS) * 180.0 / M_PI
          }
      
      
          private func pixelSpaceYToLatitude(pixelY: Double) -> Double {
              return (M_PI / 2.0 - 2.0 * atan(exp((round(pixelY) - MERCATOR_OFFSET) / MERCATOR_RADIUS))) * 180.0 / M_PI
          }
      }
      

      在视图控制器中的使用示例:

      func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
              print("Zoom: \(mapView.getZoomLevel())")
              if mapView.getZoomLevel() > 6 {
                  mapView.setCenter(coordinate: mapView.centerCoordinate, zoomLevel: 6, animated: true)
              }
          }
      

      【讨论】:

        【解决方案6】:

        如果您的目标是 iOS 13+,请使用 MKMapView setCameraZoomRange 方法。只需提供最小和最大中心坐标距离(以米为单位)。

        在此处查看 Apple 的文档:https://developer.apple.com/documentation/mapkit/mkmapview/3114302-setcamerazoomrange

        【讨论】:

          【解决方案7】:

          不要使用regionWillChangeAnimated。使用regionDidChangeAnimated

          • 我们也可以使用setRegion(region, animated: true)。通常,如果我们使用regionWillChangeAnimated,它会冻结MKMapView,但使用regionDidChangeAnimated 它可以完美运行

            func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
              mapView.checkSpan()
            }
            
            extension MKMapView {
              func zoom() {
                let region = MKCoordinateRegionMakeWithDistance(userLocation.coordinate, 2000, 2000)
                setRegion(region, animated: true)
              }
            
              func checkSpan() {
                let rect = visibleMapRect
                let westMapPoint = MKMapPointMake(MKMapRectGetMinX(rect), MKMapRectGetMidY(rect))
                let eastMapPoint = MKMapPointMake(MKMapRectGetMaxX(rect), MKMapRectGetMidY(rect))
            
                let distanceInMeter = MKMetersBetweenMapPoints(westMapPoint, eastMapPoint)
            
                if distanceInMeter > 2100 {
                  zoom()
                }
              }
            }
            

          【讨论】:

            【解决方案8】:

            使用此示例锁定最大缩放范围,同样您可以限制最小范围

            map.cameraZoomRange = MKMapView.CameraZoomRange(maxCenterCoordinateDistance: 1200000)

            【讨论】:

              【解决方案9】:

              MKMapView 内部有一个MKScrollView(私有API),它是UIScrollView 的子类。这个MKScrollView 的代表是它自己的mapView

              所以,为了控制最大缩放,请执行以下操作:

              创建MKMapView的子类:

              MapView.h

              #import <UIKit/UIKit.h>
              #import <MapKit/MapKit.h>
              
              @interface MapView : MKMapView <UIScrollViewDelegate>
              
              @end
              

              地图视图.m

              #import "MapView.h"
              
              @implementation MapView
              
              -(void)scrollViewDidZoom:(UIScrollView *)scrollView {
              
                  UIScrollView * scroll = [[[[self subviews] objectAtIndex:0] subviews] objectAtIndex:0];
              
                  if (scroll.zoomScale > 0.09) {
                      [scroll setZoomScale:0.09 animated:NO];
                  }
              
              }
              
              @end
              

              然后,访问滚动子视图并查看zoomScale 属性。当缩放大于数字时,设置最大缩放。

              【讨论】:

              • 嗯,不错。顺便说一句,您是否检查过此解决方案是否适用于应用商店?
              • 可能是这样。我将在下周提交一个使用它的应用程序。但是我已经为 UIWebView 和 UITabBar 提交了类似方法的应用程序,并且都被批准了。
              • iOS6 的一切都变了。现在不行了。未调用 scrollViewDidZoom。
              • 我会劝阻任何人不要使用私有 API。从来都不是一个好主意。
              【解决方案10】:

              Raphael Petegrosso 发布的带有扩展 MKMapView 的帖子在进行一些小的修改后效果很好。 下面的版本也更加“用户友好”,因为一旦用户松开屏幕,它就会优雅地“捕捉”回定义的缩放级别,感觉类似于 Apple 自己的弹性滚动。

              编辑:这个解决方案不是最优的,会破坏/损坏地图视图,我在这里找到了一个更好的解决方案:How to detect any tap inside an MKMapView。这使您可以拦截捏合和其他动作。


              MyMapView.h

              #import <MapKit/MapKit.h>
              
              
              @interface MyMapView : MKMapView <UIScrollViewDelegate>
              @end
              

              MyMapView.m

              #import "MyMapView.h"
              
              @implementation MyMapView
              
              - (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale
              {
                  if (scale > 0.001)
                  {
                      [scrollView setZoomScale:0.001 animated:YES];
                  }
              }
              @end
              

              对于硬限制,请使用:

              #import "MyMapView.h"
              
              @implementation MyMapView
              
              -(void)scrollViewDidZoom:(UIScrollView *)scrollView
              {
                  if (scrollView.zoomScale > 0.001)
                  {
                      [scrollView setZoomScale:0.001 animated:NO];
                  }
              
              }
              
              @end
              

              【讨论】:

                【解决方案11】:

                以下代码对我有用,并且在概念上易于使用,因为它根据以米为单位的距离设置区域。 代码来源于@nevan-king 发布的答案和@Awais-Fayyaz 发布的使用 regionDidChangeAnimated 的评论

                将以下扩展添加到您的 MapViewDelegate

                var currentLocation: CLLocationCoordinate2D?
                
                extension MyMapViewController: MKMapViewDelegate {
                    func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
                        if self.currentLocation != nil, mapView.region.longitudinalMeters > 1000 {
                            let initialLocation = CLLocation(latitude: (self.currentLocation?.latitude)!,
                                                         longitude: (self.currentLocation?.longitude)!)
                            let coordinateRegion = MKCoordinateRegionMakeWithDistance(initialLocation.coordinate,
                                                                                  regionRadius, regionRadius)
                            mapView.setRegion(coordinateRegion, animated: true)
                        }
                    }
                }
                

                然后为 MKCoordinateRegion 定义一个扩展,如下所示。

                extension MKCoordinateRegion {
                    /// middle of the south edge
                    var south: CLLocation {
                        return CLLocation(latitude: center.latitude - span.latitudeDelta / 2, longitude: center.longitude)
                    }
                    /// middle of the north edge
                    var north: CLLocation {
                        return CLLocation(latitude: center.latitude + span.latitudeDelta / 2, longitude: center.longitude)
                    }
                    /// middle of the east edge
                    var east: CLLocation {
                        return CLLocation(latitude: center.latitude, longitude: center.longitude + span.longitudeDelta / 2)
                    }
                    /// middle of the west edge
                    var west: CLLocation {
                        return CLLocation(latitude: center.latitude, longitude: center.longitude - span.longitudeDelta / 2)
                    }
                    /// distance between south and north in meters. Reverse function for MKCoordinateRegionMakeWithDistance
                    var latitudinalMeters: CLLocationDistance {
                        return south.distance(from: north)
                    }
                    /// distance between east and west in meters. Reverse function for MKCoordinateRegionMakeWithDistance
                    var longitudinalMeters: CLLocationDistance {
                        return east.distance(from: west)
                    }
                }
                

                上面的 MKCoordinateRegion 的 sn-p 是由@Gerd-Castan 在这个问题上发布的:

                Reverse function of MKCoordinateRegionMakeWithDistance?

                【讨论】:

                  【解决方案12】:

                  我在工作中遇到了这个问题,并且在没有设置全局限制的情况下创建了一些运行良好的东西。

                  我利用的 MapView 代表是: - mapViewDidFinishRendering - mapViewRegionDidChange

                  我的解决方案背后的前提是,由于卫星视图呈现一个没有数据的区域,所以它始终是同一件事。这个可怕的图像 (http://imgur.com/cm4ou5g) 如果我们可以放心地依赖该失败案例,我们可以将其用作确定用户所看到内容的关键。地图渲染后,我截取渲染的地图边界并确定平均 RGB 值。根据该 RGB 值,我假设相关区域没有数据。如果是这种情况,我会将地图弹出回正确渲染的最后一个跨度。

                  我唯一的全局检查是当它开始检查地图时,您可以根据需要增加或减少该设置。下面是完成此操作的原始代码,并将组合一个示例项目以供贡献。您可以提供的任何优化将不胜感激,希望对您有所帮助。

                  @property (assign, nonatomic) BOOL isMaxed;
                  @property (assign, nonatomic) MKCoordinateSpan lastDelta;
                  
                  self.lastDelta = MKCoordinateSpanMake(0.006, 0.006);
                  
                  - (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
                      if (mapView.mapType != MKMapTypeStandard && self.isMaxed) {
                              [self checkRegionWithDelta:self.lastDelta.longitudeDelta];
                      }
                  }
                  
                  
                  - (void)checkRegionWithDelta:(float)delta {
                      if (self.mapView.region.span.longitudeDelta < delta) {
                          MKCoordinateRegion region = self.mapView.region;
                          region.span = self.lastDelta;
                          [self.mapView setRegion:region animated:NO];
                      } else if (self.mapView.region.span.longitudeDelta > delta) {
                          self.isMaxed = NO;
                      }
                  }
                  
                  
                  - (void)mapViewDidFinishRenderingMap:(MKMapView *)mapView fullyRendered:(BOOL)fullyRendered {
                      if (mapView.mapType != MKMapTypeStandard && !self.isMaxed) {
                          [self checkToProcess:self.lastDelta.longitudeDelta];
                      }
                  }
                  
                  
                  - (void)checkToProcess:(float)delta {
                      if (self.mapView.region.span.longitudeDelta < delta) {
                          UIGraphicsBeginImageContext(self.mapView.bounds.size);
                          [self.mapView.layer renderInContext:UIGraphicsGetCurrentContext()];
                          UIImage *mapImage = UIGraphicsGetImageFromCurrentImageContext();
                          [self processImage:mapImage];
                      }
                  }
                  
                  
                  - (void)processImage:(UIImage *)image {
                      self.mapColor = [self averageColor:image];
                      const CGFloat* colors = CGColorGetComponents( self.mapColor.CGColor );
                      [self handleColorCorrection:colors[0]];
                  }
                  
                  
                  - (void)handleColorCorrection:(float)redColor {
                      if (redColor < 0.29) {
                          self.isMaxed = YES;
                          [self.mapView setRegion:MKCoordinateRegionMake(self.mapView.centerCoordinate, self.lastDelta) animated:YES];
                      } else {
                          self.lastDelta = self.mapView.region.span;
                      }
                  }
                  
                  
                  - (UIColor *)averageColor:(UIImage *)image {
                      CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
                      unsigned char rgba[4];
                      CGContextRef context = CGBitmapContextCreate(rgba, 1, 1, 8, 4, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
                  
                      CGContextDrawImage(context, CGRectMake(0, 0, 1, 1), image.CGImage);
                      CGColorSpaceRelease(colorSpace);
                      CGContextRelease(context);
                  
                      if(rgba[3] > 0) {
                          CGFloat alpha = ((CGFloat)rgba[3])/255.0;
                          CGFloat multiplier = alpha/255.0;
                          return [UIColor colorWithRed:((CGFloat)rgba[0])*multiplier
                                                 green:((CGFloat)rgba[1])*multiplier
                                                  blue:((CGFloat)rgba[2])*multiplier
                                                 alpha:alpha];
                      }
                      else {
                          return [UIColor colorWithRed:((CGFloat)rgba[0])/255.0
                                                 green:((CGFloat)rgba[1])/255.0
                                                  blue:((CGFloat)rgba[2])/255.0
                                                 alpha:((CGFloat)rgba[3])/255.0];
                      }
                  }
                  

                  【讨论】:

                  • 感谢帮助格式化弗拉基米尔,为大家道歉。