MKMapView setRegion animated does not show animation
Asked Answered
H

5

9

I have a MKMapView with annotation pins. When the view was loaded the nearest pin gets searched and the map will get zoomed so it shows both, the user's location and the nearest pin. I do that with [map setRegion:region animated:YES];. Everything works fine up to here. The same method is also called by tapping on a button which locates the user and then does exactly what I just described.

I also have a search field with which the user can search for map points. When the user taps on one of the search results the map sets the region so the searched pin is in the middle. Now, there's something strange with that. I also set this region animated, at least I do the same command as above. But if the map point is too far away from the current visible part of the map it doesn't show the animation when changing the region. Am I missing something? I've already had a look at Apples docs, they don't mention anything regarding any maximum distance for animations.

I'm looking forward to any help!


Update 1:
Just tested it again in the Simulator. An interesting fact is, that when I search for a MapPoint for the first time and then select a search result it doesn't animate. If I perform another search just after the first one and select a result it does animate. As soon as I tap on the locate button which brings the user back to his location and the closest point it doesn't animate for this setRegion: and the first search after that. But only in the Simulator, on my 4S it does exactly what I've described in the original question above.


Update 2:
In the comments I was asked to provide example coordinates.
So here the coordinates for the first step (searching of the own location and the nearest pin):
My Location: 47.227131 / 8.264251
Nearest pin: 47.251347 / 8.357191
The distance between them is about 22 kilometers. The center of the map is the center between the two pins. The distance from the center to the screen border is 1.5 times the distance between the two points which means about 33 kilometers in this case.
And here a set of coordinates for the second step (searching a map point and selecting it):
Searched pin: 46.790680 / 9.818824
The distance to the screen border is here fixed to 500 meters.

Haemic answered 16/4, 2013 at 15:32 Comment(14)
Have you tried using a MKMapViewDelegate and seeing how often mapView:regionDidChangeAnimated: is called and what animated is set to?Pasteurizer
@Pasteurizer Yes, I'm using the delegate methods and I already tested that: the animated attribute is NO... It seems like iOS changes that anywhere, but it doesn't make sense to me...Haemic
@Pasteurizer correction: not always NO, just if it doesn't set the region animated...Haemic
at least it's consistent with what you're seeing. Are you calling setRegion:animated on the main thread of your app?Pasteurizer
@Pasteurizer Yes, it's on the main thread. It's what I assumed first too, but I printed the current thread to the log before calling setRegion:animated and it's called on the main thread.Haemic
are you sure about the region you are trying to set, does actually encompass the pin that needs to be visible?Elenore
Yes. They all have titles and it jumps to the one I selected in when searching. Also it jumps to the nearest at the beginning. But anyway how does that affect animation?Haemic
check the MKCoordinateSpanmaybe for huge distances isn't enough with the current one (property of MKCoordinateRegion)Uniseptate
maybe there's someone else calling setRegion:animated: with NO, you can intercept those calls subclassing MKMapView and overriding that method or adding a symbolic breakpoint with symbol: -[MKMapView setRegion:animated:]Patrol
@Uniseptate Will definitively try that! Thanks!Haemic
@Patrol I've already checked that by printing out the calls with NSLog. It's only called when it's supposed to.Haemic
I just want to get the facts straight. 1.You start the app and it creates a region with the user location and the nearest pin. 2. You change the map region to center on another pin and the map changes but without the desired animation. Could you provide 2 sets of coordinates for the step one and 1 set for coordinate for step two.Ninth
Sure. First step: My Location: 47.227131/8.264251, nearest pin: 47.251347/8.357191 (distance between them is about 22 kilometers, distance to the screen border is 1.5 times the distance). Second step: 46.790680/9.818824 (distance to the screen border is here fixed to 500 meters).Haemic
Added an update 2 with some more details than I provided in the comment.Haemic
T
14

I've tested this issue with a simple demo application on iOS 6 and iOS 7 beta. It turns out that the map view actually not always animates the transition between regions. It depends on how far the regions lay apart. For example a transition from Paris to London is not animated. But if you first zoom out a little bit and then go to London it will be animated.

The documentation says:

animated: Specify YES if you want the map view to animate the transition to the new region or NO if you want the map to center on the specified region immediately.

But as we've seen, we can not rely on the animation. We can only tell the map view that the transition should be animated. MapKit decides whether an animation is appropriate. It tells the delegate if the transition will be animated in -(void)mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated.

In order to consistently animate the region change in all cases you will need to animate to a intermediate region first. Let A be the current map region and B the target region. If there is an intersection between the regions you can transition directly. (Transform the MKCoordinateRegion to an MKMapRect and use MKMapRectIntersection to find the intersection). If there is no intersection, calculate a region C that spans both regions (use MKMapRectUnion and MKCoordinateRegionForMapRect). Then first go to to region C and in regionDidChangeAnimated go to region B.

Sample code:

MKCoordinateRegion region = _destinationRegion;    
MKMapRect rect = MKMapRectForCoordinateRegion(_destinationRegion);
MKMapRect intersection = MKMapRectIntersection(rect, _mapView.visibleMapRect);
if (MKMapRectIsNull(intersection)) {
    rect = MKMapRectUnion(rect, _mapView.visibleMapRect);
    region = MKCoordinateRegionForMapRect(rect);
    _intermediateAnimation = YES;
}
[_mapView setRegion:region animated:YES];

-(void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated
{
    if (_intermediateAnimation) {
        _intermediateAnimation = NO;
        [_mapView setRegion:_destinationRegion animated:YES];
    }
}

This helper method is taken from here

MKMapRect MKMapRectForCoordinateRegion(MKCoordinateRegion region)
{
    MKMapPoint a = MKMapPointForCoordinate(CLLocationCoordinate2DMake(
                                                                      region.center.latitude + region.span.latitudeDelta / 2,
                                                                      region.center.longitude - region.span.longitudeDelta / 2));
    MKMapPoint b = MKMapPointForCoordinate(CLLocationCoordinate2DMake(
                                                                      region.center.latitude - region.span.latitudeDelta / 2,
                                                                      region.center.longitude + region.span.longitudeDelta / 2));
    return MKMapRectMake(MIN(a.x,b.x), MIN(a.y,b.y), ABS(a.x-b.x), ABS(a.y-b.y));
}

The WWDC 2013 session 309 Putting Map Kit in Perspective explains how to do such complex transitions in iOS 7.

Transcalent answered 28/6, 2013 at 11:18 Comment(1)
That sounds reasonable! I already found out that it animates if I set the region to the initial situation first. But as that looks pretty bad I removed it again. But first finding the (smallest) intersection region looks much better. Thanks very much for that solution!Haemic
B
2

Here are the functions by @Felix rewritten to Swift 4:

// MARK: - MapView help properties
var destinationRegion: MKCoordinateRegion?
var intermediateAnimation = false

func center() {
    // Center map
    var initialCoordinates = CLLocationCoordinate2D(latitude: 49.195061, longitude: 16.606836)
    var regionRadius: CLLocationDistance = 5000000

    destinationRegion = MKCoordinateRegionMakeWithDistance(initialCoordinates, regionRadius * 2.0, regionRadius * 2.0)
    centreMap(on: destinationRegion!)
}


private func mapRect(forCoordinateRegion region: MKCoordinateRegion) -> MKMapRect {
    let topLeft = CLLocationCoordinate2D(latitude: region.center.latitude + (region.span.latitudeDelta/2), longitude: region.center.longitude - (region.span.longitudeDelta/2))
    let bottomRight = CLLocationCoordinate2D(latitude: region.center.latitude - (region.span.latitudeDelta/2), longitude: region.center.longitude + (region.span.longitudeDelta/2))

    let a = MKMapPointForCoordinate(topLeft)
    let b = MKMapPointForCoordinate(bottomRight)

    return MKMapRect(origin: MKMapPoint(x:min(a.x,b.x), y:min(a.y,b.y)), size: MKMapSize(width: abs(a.x-b.x), height: abs(a.y-b.y)))
}


func centreMap(on region: MKCoordinateRegion) {
    var region = region
    var rect = mapRect(forCoordinateRegion: region)
    let intersection = MKMapRectIntersection(rect, mapView.visibleMapRect)

    if MKMapRectIsNull(intersection) {
        rect = MKMapRectUnion(rect, mapView.visibleMapRect)
        region = MKCoordinateRegionForMapRect(rect)
        intermediateAnimation = true
    }

    mapView.setRegion(region, animated: true)
}

// MARK: - MKMapViewDelegate
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
    if intermediateAnimation, let region = destinationRegion {
        intermediateAnimation = false
        mapView.setRegion(region, animated: true)
    }
}
Buncombe answered 9/12, 2017 at 16:46 Comment(0)
C
1

I also had this problem where it would not always animate, sometimes it would just jump and dissolve instead. However, I noticed that if you animate the camera instead of the region, it consistently animates.

But using the camera, you have to set the eye distance/altitude instead of the lat/lon span. I have a simple calculation for that below which is rough, it basically just sets the altitude (in meters) to the same number of meters as the longitude span of the region. If you wanted exact accuracy you'd have to figure out the number of meters per degree for the region's latitude, which changes slightly because the earth is not a perfect sphere. Of course you could multiply that value to widen or narrow the view to taste.

Swift 4.1 example code:

/* -------------------------------------------------------------------------- */
func animateToMapRegion(_ region: MKCoordinateRegion) {
    // Quick and dirty calculation of altitude necessary to show region.
    // 111 kilometers per degree longitude.
    let metersPerDegree: Double = 111 * 1_000
    let altitude = region.span.longitudeDelta * metersPerDegree

    let camera = MKMapCamera(lookingAtCenter: region.center, fromEyeCoordinate: region.center, eyeAltitude: altitude)

    self.mapView.setCamera(camera, animated: true)
}
Cupreous answered 3/5, 2018 at 22:38 Comment(2)
Instead of using longitude, use latitude, whose value of distance per degree doesn't change. For locations that are relatively close to each other, the ratio lat/long will not significantly change. I'm guessing the location is just as likely to change latitude as longitude, so this should be just as good.Episiotomy
Better yet, convert the two points to CLLocations so you can get their distance in meters using the distance(from:) method.Episiotomy
S
1

You simply have to get your current location and then call this function:

import MapKit

var appleMapView = MKMapView()

var currentCoordinate: CLLocationCoordinate2D?

currentCoordinate must be your current location coordinates:

 ◙ if let currentLoc = self.currentCoordinate {
        let center = CLLocationCoordinate2D(latitude: currentLoc.latitude, longitude: currentLoc.longitude)
        let region = MKCoordinateRegion(center: center, span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01))
        appleMapView.setRegion(region, animated: true)
    }
Superhuman answered 27/11, 2020 at 13:47 Comment(1)
Well explained.Forget
T
0

To anyone who has the same question and is using Swift and is using a tableView:

I called setRegion after dismissing the tableView, and it did not show animation. This is my code before editing:

dismiss(animated: true, completion: nil)
a function that calls setRegion

Then I changed it to:

dismiss(animated: true, completion: {
        a function that calls setRegion
    })

This time it works.

Thekla answered 15/10, 2017 at 22:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.