MKScaleView doesn't update changing area programmatically

I have a MKMapView with a MKScaleView. If I visualise a generic map I have the scale in km. When I change the MKMapRect using visibleMapRect, the scale doesn't change. If I use setVisibleMapRect(_ mapRect: MKMapRect, animated animate: Bool), the scale change but not to the correct one. For example, it shows a scale saying one inch corresponds to 250 m while it is 150 m. The same issue of I use MKCoordinateRegion. Instead, if I zoom in or zoom out pinching on the map, the scale updates correctly.

Am I doing something wrong? How can I fix this?

Sample code:

import UIKit
import MapKit

let CORNER_RADIUS: CGFloat = 8.0
let METERS_PER_MILE: Double = 1609.344

class PIAnnotation: NSObject, MKAnnotation {
    
    var coordinate: CLLocationCoordinate2D
    
    private(set) var title: String?
    private(set) var subtitle: String?
    
    init(location: CLLocationCoordinate2D,
         title: String? = nil, subtitle: String? = nil) {
        
        coordinate = location
        self.title = title
        self.subtitle = subtitle
    }
}

class PISimpleMapView: MKMapView {
    
    private let HALF_MAP_SIDE_MULTIPLIER: Double = 1.4
    
    private let pinIdentifier = "pinIdentifier"
    
    private var scaleView: MKScaleView?
    
    typealias PinAnnotationView = MKMarkerAnnotationView     // MKPinAnnotationView
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        inizialize()
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        inizialize()
    }
    
    func inizialize() {
        
        layer.cornerRadius = CORNER_RADIUS
        register(PinAnnotationView.self,
                 forAnnotationViewWithReuseIdentifier: pinIdentifier)
        
        addScale()
    }
    
    private func addScale() {
        
        let scale = MKScaleView(mapView: self)
        scale.translatesAutoresizingMaskIntoConstraints = false
        scale.scaleVisibility = .visible // always visible
        addSubview(scale)
        
        let guide = safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            scale.leftAnchor.constraint(equalTo: guide.leftAnchor, constant: 16.0),
            scale.rightAnchor.constraint(equalTo: guide.centerXAnchor),
            scale.topAnchor.constraint(equalTo: guide.topAnchor),
            scale.heightAnchor.constraint(equalToConstant: 20.0)
        ])
        
        scaleView?.removeFromSuperview()
        scaleView = scale
    }
    
    func displayPinOnMap(location: CLLocation) {
        
        let annotation = PIAnnotation(location: location.coordinate,
                                      title: "Sample", subtitle: nil)
        addAnnotation(annotation)
        
        // Position the map so that all overlays and annotations are visible on screen.
        visibleMapRect = visibleArea(from: annotation)
        // setVisibleMapRect(visibleArea(from: annotation), animated: true)
        // region = MKCoordinateRegion(visibleArea(from: annotation))
    }
    
    private func visibleArea(from annotation: PIAnnotation) -> MKMapRect {
        
        let annotationPoint = MKMapPoint(annotation.coordinate)
        return MKMapRect(x: annotationPoint.x - HALF_MAP_SIDE_MULTIPLIER * METERS_PER_MILE,
                         y: annotationPoint.y - HALF_MAP_SIDE_MULTIPLIER * METERS_PER_MILE,
                         width: HALF_MAP_SIDE_MULTIPLIER * 2.0 * METERS_PER_MILE,
                         height: HALF_MAP_SIDE_MULTIPLIER * 2.0 * METERS_PER_MILE)
    }
}

This code stands out to me due to its use of various constants over a MKMapRect:

private func visibleArea(from annotation: PIAnnotation) -> MKMapRect {
        
        let annotationPoint = MKMapPoint(annotation.coordinate)
        return MKMapRect(x: annotationPoint.x - HALF_MAP_SIDE_MULTIPLIER * METERS_PER_MILE,
                         y: annotationPoint.y - HALF_MAP_SIDE_MULTIPLIER * METERS_PER_MILE,
                         width: HALF_MAP_SIDE_MULTIPLIER * 2.0 * METERS_PER_MILE,
                         height: HALF_MAP_SIDE_MULTIPLIER * 2.0 * METERS_PER_MILE)
    }

It's very easy to make geometry mistakes like this. It's far better to always use MapKit's built-in geometry functions (those defined in MKGeometry.h), and to specify the view of the map that you intend through either an MKCoordinateRegion or an MKMapCamera, as both take coordinates based in the real world — coordinates, meters, and degrees. By using values like coordinates and meters, they are semantically easier to deal with for manipulating the visible portion of the map. Unless you're extremely familiar with projected coordinates, it's very easy to make mistakes that are hard to spot, as values for projected coordinates are much harder to intuitively reason about compared to coordinates and meters.

Further, the value for visibleMapRect is not always entirely visible. For example, if you use a map camera to show a pitched map, the area of the map visible within the map view's frame is trapezoidal, so the value of visibleMapRect is the rectangle fully containing that trapezoidal shape, which also means there's some areas inside that rectangle outside of the trapezoid. This may contribute to the scale view being different than you are expecting. That's why it's better to express your actual intent to MapKit — such as a map camera looking at x coordinate, with y altitude and z pitch — and let MapKit handle the math to the projected map rect for you.

MKMapRect is most useful when dealing with customized overlays, as then you have a consistent rectilinear coordinate system for the convenience of drawing an overlay the same way you'd draw anything custom with Core Graphics.

With that background set, please do let me know if you still find that the scale view is not showing the values you expect after you convert your geometry code as I suggest above.

—Ed Ford,  DTS Engineer

Hi Ed,

thank you for your answer.

I tried to specify the view of the map through an MKCoordinateRegion, and I often get the same problem (but not always). When I zoom in from a large area (say 150km per cm scale) to my small region of interest, the scale says it is 2cm to 2.5km, but if I pan around a little, the scale immediately changes to 2cm to 1.25km. It seems to me that the scale doesn't adapt to the last zoom. In fact, if I disable the animation, when I zoom in to the small region area the map scale remains at the initial value.

let mapRegion = MKCoordinateRegion(
    center: annotation.coordinate,
    span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
)
setRegion(mapRegion, animated: true)

The situation improves with MKMapCamera: the scale is correct after zooming, but it only changes if I use animation.

let placemark = MKPlacemark(coordinate: annotation.coordinate)
let mapItem = MKMapItem(placemark: placemark)
let camera = MKMapCamera(
    lookingAt: mapItem,
    forViewSize: CGSize(
        width: HALF_MAP_SIDE_MULTIPLIER * 2.0 * METERS_PER_MILE,
        height: HALF_MAP_SIDE_MULTIPLIER * 2.0 * METERS_PER_MILE
    ),
    allowPitch: false
)
setCamera(camera, animated: true)

Do you have a small buildable Xcode test project that I can build to see the same thing you are? I'd appreciate if you could share a link here to that project so that we have a project configured with the exact same configuration, including the portion of the map you're using, as well your configuration for the map view's bounds or frame.

In your second example, the multiplication including a constant METERS_PER_MILE seems like it won't produce the results you expect because that's being used with a view geometry structure, not map geometry. Your first example with pure MKCoordinateRegion is what I'd expect to see in code, so that's where a buildable test project to see the same results you are is needed.

—Ed Ford,  DTS Engineer

Hi Ed,

I added a simple project on GitHub to test the MKScaleView's behaviour; it is available here: https://github.com/asclepix/MapTest.git.

I noticed that if the zooming-in starts from a not so small-scale (for example 1:25.000, "Zoom Out" button in the sample), the Zoom In finishes with the right scale. If the zooming-in starts from a smaller scale ("World Zoom") the final scale is usually not the right one.

By the way, using the iOS 18 simulator, the large scale map has a red background, while it is not the case on simulators of older operating systems or on a real device with iOS 18.

I can reproduce what you are seeing. Try adding the following to the map view delegate:

func mapView(
  _ mapView: MKMapView,
  regionDidChangeAnimated animated: Bool)
  {
  addScale()
  }

I tried your code, making PISimpleMapView its own delegate, and it seems to fix the problem.

MKScaleView doesn't update changing area programmatically
 
 
Q