MKMapView show incorrectly saved region

2019-01-09 12:16发布

问题:

I'm saving map region into user defaults when my iPhone app is closing like this:

MKCoordinateRegion region = mapView.region;
[[NSUserDefaults standardUserDefaults] setDouble:region.center.latitude forKey:@"map.location.center.latitude"];
[[NSUserDefaults standardUserDefaults] setDouble:region.center.longitude forKey:@"map.location.center.longitude"];
[[NSUserDefaults standardUserDefaults] setDouble:region.span.latitudeDelta forKey:@"map.location.span.latitude"];
[[NSUserDefaults standardUserDefaults] setDouble:region.span.longitudeDelta forKey:@"map.location.span.longitude"];

When app launches again, Ш read those values back the same way, so that the user can see exactly the same map view as it was last time:

MKCoordinateRegion region;

region.center.latitude  = [[NSUserDefaults standardUserDefaults] doubleForKey:@"map.location.center.latitude"];
region.center.longitude = [[NSUserDefaults standardUserDefaults] doubleForKey:@"map.location.center.longitude"];
region.span.latitudeDelta  = [[NSUserDefaults standardUserDefaults] doubleForKey:@"map.location.span.latitude"];
region.span.longitudeDelta = [[NSUserDefaults standardUserDefaults] doubleForKey:@"map.location.span.longitude"];

NSLog([NSString stringWithFormat:@"Region read  : %f %f %f %f", region.center.latitude, region.center.longitude, region.span.latitudeDelta, region.span.longitudeDelta]);

[mapView setRegion:region];

NSLog([NSString stringWithFormat:@"Region on map: %f %f %f %f", mapView.region.center.latitude, mapView.region.center.longitude, mapView.region.span.latitudeDelta, mapView.region.span.longitudeDelta]);

The region I read from user defaults is (not surprisingly) exactly the same as when it was saved. Notice that what is saved comes directly from the map, so it's not transformed in any way. I set it back on map with setRegion: method, but then it is different!

Example results:

Region read  : 50.241110 8.891555 0.035683 0.042915<br>
Region on map: 50.241057 8.891544 0.050499 0.054932

Does anybody know why this happens?

回答1:

The issue here is when you set the region, the map zoom level "snaps" out to the nearest zoom threshold. (I suspect these zoom thresholds are the amounts of zoom you get when you double-tap or two-finger-tap)

So if the map is showing zoom level 1 for instance, and you set the region to that same span value thusly: region = [mapView region]; [mapView setRegion:region]; it will "snap" out to the nearest zoom level above level 1, i.e. level 2 and you will zoom out by about a factor of two.

The workaround for the original poster is to reduce the span values slightly before setting the region, so that when the view snaps out, it snaps out to the zoom level it was on, not the one above.

e.g.

region.span.latitudeDelta = [[NSUserDefaults standardUserDefaults] doubleForKey:@"map.location.span.latitude"] * 0.999;

region.span.longitudeDelta = [[NSUserDefaults standardUserDefaults] doubleForKey:@"map.location.span.longitude"] * 0.999;

If the user had been zooming with double-taps (and hence jumping from threshold to threshold) this works pretty well, returning them to the same view almost exactly.

However if they pinch-zoom and the view is halfway between the zoom thresholds it will still snap out to the next level. Not so good in that case but there is no fix as yet.

There are open bugs on Apple radar for this, hopefully it will be fixed in a future release.



回答2:

OK, so I've been struggling with this issue for some time now and I think that I've come up with an effective workaround inspired by Crufty's theory (MKMapView show incorrectly saved region). Basically, if you set the map view region with the values that you fetch from your NSUserDefaults after the map view has done its initial load (complete with "snap out" behavior), the map region will be what you'd expect it to be. The trick is to find a hook in your application code somewhere downstream from the map view having been initialized. Not pretty, but it works perfectly for me. Thanks to Crufty for the insight.



回答3:

I had the same problem. This is very frustrating. It seems that the documentation for MKMapView is incorrect in some areas regarding datatypes.

If you set the region parameters as (double)s you'll get the error you're having. However if the region parameters are passed (float)s you'll get the correct behavior.

So try

MKCoordinateRegion region = {{0.0f,0.0f},{0.0f,0.0f}};

region.center.latitude = (float) [[NSUserDefaults standardUserDefaults] doubleForKey:@"map.location.center.latitude"];
region.center.longitude = (float) [[NSUserDefaults standardUserDefaults] doubleForKey:@"map.location.center.longitude"];
region.span.latitudeDelta = (float) [[NSUserDefaults standardUserDefaults] doubleForKey:@"map.location.span.latitude"];
region.span.longitudeDelta = (float) [[NSUserDefaults standardUserDefaults] doubleForKey:@"map.location.span.longitude"];

mapView.region = region;


回答4:

I can't verify that this works. Can anyone else do so? I'd be willing to bet that what you're seeing is the effect of truncation of your doubles into floats. In some instances, when this happens to me, I've found I can multiply the span by 0.9999 and reset the region, and then it'll be what I want. But not all regions - sometimes it's very different, up to 20% different, especially for small spans.



回答5:

I've found greater success using a multiplication factor of 0.9 instead of 0.999 it still sometimes pushes out but seems to hug closer to the desired zoom. Would still love to solve this issue just to say i beat it! haha



回答6:

Try also to store attitude like

self.lastCameraAtitude = self.mapView.camera.altitude;

and when u configure your map

[self.mapView.camera setAltitude:self.lastCameraAtitude];

This will set for same region same camera. In your case u only set region but not the camera.



回答7:

Here's a Swift extension to properly encode and decode MKCoordinateRegion:

extension MKCoordinateRegion {

    var encode:[String: AnyObject] {
        return ["center":
                   ["latitude": self.center.latitude,
                   "longitude": self.center.longitude],
                "span":
                   ["latitudeDelta": self.span.latitudeDelta,
                   "longitudeDelta": self.span.longitudeDelta]]
    }

    init?(decode: [String: AnyObject]) {

        guard let center = decode["center"] as? [String: AnyObject],
            let latitude = center["latitude"] as? Double,
            let longitude = center["longitude"] as? Double,
            let span = decode["span"] as? [String: AnyObject],
            let latitudeDelta = span["latitudeDelta"] as? Double,
            let longitudeDelta = span["longitudeDelta"] as? Double
        else { return nil }


        self.center = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
        self.span = MKCoordinateSpan(latitudeDelta: latitudeDelta, longitudeDelta: longitudeDelta)
    }
}

This is how to use it:

// Save
NSUserDefaults.standardUserDefaults().setObject(mapView.region.encode, forKey: "mapRegion01")

// Restore
if let dict = NSUserDefaults.standardUserDefaults().dictionaryForKey("mapRegion01"),
    let myRegion = MKCoordinateRegion(decode: dict) {

    // do something with myRegion
}


回答8:

Recently I hit the same thing. Using the insights from earlier answers, the following swift 4 implementation works for me:

override func viewWillAppear(_ animated: Bool) {
    if isMovingToParentViewController {
        setInitialRegion()
        DispatchQueue.main.asyncAfter(deadline: .now(), execute: {
            self.setInitialRegion()
        })
    }
}

This sets an initial zoom region when the map view controller is pushed into view. The synchronous call to setInitialRegion will set a region snapped to a zoom level. The asynchronous call to setInitialRegion will set the exact region. The synchronous call is still needed to remove zoom artifacts.



回答9:

If you set up the MapView in InterfaceBuilder, make sure you don't do this:

_mapView = [[MKMapView alloc] init];

As soon as I removed this init line, my map view suddenly began responding properly to all the updates I sent it. I suspect that what happens is that if you do the alloc init, it's actually creating another view that's not being shown anywhere. The one you see on the screen is the one initialized by your nib. But if you alloc init a new one, then that's something somewhere else and it's not going to do anything.