MKMapView setRegion animated does not show animation - iphone

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.

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.

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

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

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

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.

Related

Centering a map annotation to the top quarter of the screen

so I'd like to go from image 1 to image 2 when an annotation is clicked (Mapbox):
https://i.stack.imgur.com/OFIFa.png
It's fairly easy to have the map center on the annotation, by calling mapView.setCenter() inside one of the Mapbox delegate functions as follows:
func mapView(_ mapView: MGLMapView, didSelect annotation: MGLAnnotation) {
mapView.setCenter(annotation.coordinate, animated: true)
}
Obviously, this centers the annotation to the middle of the screen, but I need the annotation to be 'centered' to in the area above the view that pops up so it's still visible (ideally, it'll be equidistant from the top of 'View' and the top edge of the screen).
I was thinking of setting the zoomLevel in setCenter and then zooming to a specific distance south of the annotation, but the problem with this is various screen sizes on iOS devices will have the annotation centered differently.
I was also thinking maybe I could do some kind of coordinate conversion from the map to a CGPoint on the screen but am really unsure about how to implement this correctly (I've only ever used mapView.convert(CGPoint,toCoordinateFrom: mapView), and this won't be useful here). I'm not sure how to approach this problem. Any help would be much appreciated, whether it's just getting me started on a path or if you already have a solution that's even better. Thanks!
I don't use MapBox but if you can access the region's span (horizontal and vertical distance of the map)
You can subtract the desired amount of the latitude's span from the annotation's coordinate and set that center for the map.
///Centers the map region by placing the passed annotation in the center of the I quadrant
func centerTop(annotation: SpecialAnnot){
//Find a 4th of the span (The horizontal and vertical span representing the amount of map to display.)
let latCorrection = region.span.latitudeDelta/4 + region.span.latitudeDelta/8
self.region = MKCoordinateRegion(center: .init(latitude: annotation.coordinate.latitude - latCorrection, longitude: annotation.coordinate.longitude), span: region.span)
}
Here is some SwiftUI code.
import SwiftUI
import MapKit
class SpecialAnnot: NSObject, MKAnnotation, Identifiable{
let id: UUID = .init()
var coordinate: CLLocationCoordinate2D
init(coordinate: CLLocationCoordinate2D) {
self.coordinate = coordinate
super.init()
}
}
struct QuarterCenteredView: View {
#State var region: MKCoordinateRegion = MKCoordinateRegion()
let annotationCoordinate: [SpecialAnnot] = [SpecialAnnot(coordinate: .init(latitude: 40.748817, longitude: -73.985428))]
var body: some View {
VStack{
Button("center quadrant I", action: {
centerTop(annotation: annotationCoordinate.first!)
})
Map(coordinateRegion: $region, annotationItems: annotationCoordinate, annotationContent: { annot in
MapAnnotation(coordinate: annot.coordinate, content: {
Image(systemName: "mappin").foregroundColor(.red)
})
})
.onAppear(perform: {
DispatchQueue.main.async {
region = MKCoordinateRegion(center: annotationCoordinate.first!.coordinate, span: region.span)
}
})
}
}
///Centers the map region by placing the passed annotation in the center of the I quadrant
func centerTop(annotation: SpecialAnnot){
//Find a 4th of the span (The horizontal and vertical span representing the amount of map to display.)
let latCorrection = region.span.latitudeDelta/4 + region.span.latitudeDelta/8
self.region = MKCoordinateRegion(center: .init(latitude: annotation.coordinate.latitude - latCorrection, longitude: annotation.coordinate.longitude), span: region.span)
}
}
struct QuarterCenteredView_Previews: PreviewProvider {
static var previews: some View {
QuarterCenteredView()
}
}
You could resize the map frame to that square then center the coordinate. This would mean the view would no longer pop op over the mapview though it would appear to.
this can be done with:
(your MKMapView).frame = CGRect(x: , y: , width: , height: )
x and y are start coordinates in the superview (the view behind the mapview) coordinate system. Then the width and height parameters creates a rectangle with those dimensions starting from x and y. Be careful with the start point, it's sometimes in the lowerleft and sometimes in the upper left depending on the situation. Depending on your implementation you might have to modify the popup views frame too.
-if your map is always connected to the top of the screen you could use
(your MKMapView).frame = CGRect(x: self.view.origin.x, y: self.view.origin.y, width: self.view.frame.width, height: self.view.frame.height/2)

how to keep the current location dot on the centre of the map when moving, google map, swift

In my project I am using google map. I am enabling the current location dot like this:
self.mapView?.isMyLocationEnabled = true
After that I am getting the current location latitude and longitude like this:
guard let currentLat = self.mapView?.myLocation?.coordinate.latitude,
let currentLan = self.mapView?.myLocation?.coordinate.longitude else {
showMessageFail(title: "Fail", myMessage: "Could not determine your current location")
return
}
And then focusing the camera on current location like this:
let currentPosition = CLLocationCoordinate2D(latitude: currentLat, longitude: currentLan)
self.mapView?.animate(toLocation: self.currentPosition)
Everything is perfectly fine, now it shows the current location dot (the blue dot on google map) on the centre of the map. Now when current location changed, say I am in a car the current location dot moves, at some point it moves out of the bounds of the map. How can I always keep the current location dot on the centre of the map and move the map not the dot. Any help would be really appreciated.
currLocation = manager.location!
let locationOfDevice: CLLocation = currLocation // this determines the location of the device using the users location
let deviceCoordinate: CLLocationCoordinate2D = locationOfDevice.coordinate // determines the coordinates of the device using the location device variabel which has in it the user location.
let span = MKCoordinateSpan(latitudeDelta: 1, longitudeDelta: 1) // this determines the span in which its determined that 1 degree is 69 miles.
let region = MKCoordinateRegion(center: deviceCoordinate, span: span) // set the center equal to the device coordinates and the span equal to the span variable we have created this will give you the region.
mapView.setRegion(region, animated: true);
CLLocation *newMyLocation = [[CLLocation alloc] initWithLatitude:_locationManager.location.coordinate.latitude longitude:_locationManager.location.coordinate.longitude];
_cmarker.position = newMyLocation.coordinate;
CGPoint point1 =[_mapkitView.projection pointForCoordinate:newMyLocation.coordinate];
point1.y = -400;
GMSCameraUpdate *cameraUpdate = [GMSCameraUpdate setTarget:[_mapkitView.projection coordinateForPoint:point1]];
[_mapkitView animateWithCameraUpdate:cameraUpdate];
_cmarker.map = self.mapkitView;
//[_mapkitView setCenterCoordinate:newMyLocation.coordinate zoomLevel:_mapkitView.zoomLevel animated:YES];
[_mapkitView animateToLocation:CLLocationCoordinate2DMake(newMyLocation.coordinate.latitude, newMyLocation.coordinate.longitude)];
[_mapkitView animateToZoom:_mapkitView.camera.zoom]; // animate to that zoom level

Change Initial Zoom MapKit Swift

I'm new in Swift, and I'm trying to use MkMapView. I'd like to change the initial zoom. I searched On the API but I don't find anything.
I find only this, about zoom
mapView.zoomEnabled = true
But it isn't what I looking For.
MKMaps don't have a concept of zoom as a configurable variable. Zoom enabled pertains to user interaction. You'll need to configure something manually by setting a region that encompasses your desired zoom level.
For instance, if I wanted to zoom in on some specific coordinate, I could do:
let coordinate: CLLocationCoordinate2D = // .. populate your center
let latitudinalMeters: CLLocationDistance = 50
let longitudinalMeters: CLLocationDistance = 50
let region = MKCoordinateRegionMakeWithDistance(coordinate, latitudinalMeters, longitudinalMeters)
self.mapView.setRegion(region, animated: false) // Set to yes to animate, you said initial load so I image this won't be visible anyways.
Then, adjust the latitudinal/longitudinal dimensions to meet your desired zoom level.
If you wanted, you could probably create a category that adds zoom and calls this in the background.
From Logan's answer, update for Swift 5+
let coordinate: CLLocationCoordinate2D = // .. populate your center
let latitudinalMeters: CLLocationDistance = 50
let longitudinalMeters: CLLocationDistance = 50
let region = MKCoordinateRegion(center: coordinate, latitudinalMeters: latitudinalMeters, longitudinalMeters: longitudinalMeters)
self.mapView.setRegion(region, animated: false) // Set to yes to animate, you said initial load so I image this won't be visible anyways.

how to find current zoom level of MKMapView?

I want to know the current zoom level of MKMapView in iphone programming, how can I do that?
Actually I have one app, that is taking few arguments(Radius from center to corner of MKMapView) are returning the store details in that are, when I am on MKMapView, the radius is very high and it changes when Radius is smaller, so I want to know the zoom level and setup my webservice according to that, How can I get zoom level of current MKMapView visible area?
I created very simple helper subclass for it:
#define MERCATOR_RADIUS 85445659.44705395
#define MAX_GOOGLE_LEVELS 20
#interface MKMapView (ZoomLevel)
- (double)getZoomLevel;
#end
#implementation MKMapView (ZoomLevel)
- (double)getZoomLevel
{
CLLocationDegrees longitudeDelta = self.region.span.longitudeDelta;
CGFloat mapWidthInPixels = self.bounds.size.width;
double zoomScale = longitudeDelta * MERCATOR_RADIUS * M_PI / (180.0 * mapWidthInPixels);
double zoomer = MAX_GOOGLE_LEVELS - log2( zoomScale );
if ( zoomer < 0 ) zoomer = 0;
// zoomer = round(zoomer);
return zoomer;
}
#end
You can use span inside the region property of the MKMapView. It is defined like this:
typedef struct {
CLLocationDegrees latitudeDelta;
CLLocationDegrees longitudeDelta;
} MKCoordinateSpan;
Take a look at the documentation. It is well explained there.
The easiest way to get an Integer of the current zoom level, is by using the MapView function: regionDidChangeAnimated. This function recognizes every change in zoom and will give you the basis for the calculation of the zoom factor.
Just insert this function into your MapView class (works for Swift 3.0):
var mapView: MKMapView! = nil
...
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
let zoomWidth = mapView.visibleMapRect.size.width
let zoomFactor = Int(log2(zoomWidth)) - 9
print("...REGION DID CHANGE: ZOOM FACTOR \(zoomFactor)")
}
And you will get a zoomFactor value out of it, where 0 is the most near point you can zoom into the map and every higher value is a far far away zoom... :-)
All the previous answers do not take into consideration current map rotation. MKMapView's longitudeDelta differs for non-rotated map and rotated map.
Here is a great function for straight map zoom calculation: https://stackoverflow.com/a/15020534/4923516
And here is my improvement in Swift, that takes into consideration map rotation and returns current zoom level:
class MyMapView : MKMapView {
func getZoom() -> Double {
// function returns current zoom of the map
var angleCamera = self.camera.heading
if angleCamera > 270 {
angleCamera = 360 - angleCamera
} else if angleCamera > 90 {
angleCamera = fabs(angleCamera - 180)
}
let angleRad = M_PI * angleCamera / 180 // camera heading in radians
let width = Double(self.frame.size.width)
let height = Double(self.frame.size.height)
let heightOffset : Double = 20 // the offset (status bar height) which is taken by MapKit into consideration to calculate visible area height
// calculating Longitude span corresponding to normal (non-rotated) width
let spanStraight = width * self.region.span.longitudeDelta / (width * cos(angleRad) + (height - heightOffset) * sin(angleRad))
return log2(360 * ((width / 256) / spanStraight)) + 1;
}
}
You can download sample project in my repo: https://github.com/d-babych/mapkit-wrap
If it helps you can also use mapView.camera.altitude to get the current altitude of the currently displayed map region.
This is what helped me when I searched how to get the map zoom level.

Convert span value into meters on a mapview

Whenever the user zoom in or out the map i need to know how many meters are currently represented on the map (width or height).
What i need is the inverse function of MKCoordinateRegionMakeWithDistance to calculate the distance represented by the current map span.
I tried the following code but i get wrong results :
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
MKMapRect mRect = self.map.visibleMapRect;
MKMapPoint northMapPoint = MKMapPointMake(MKMapRectGetMidX(mRect), MKMapRectGetMinY(mRect));
MKMapPoint southMapPoint = MKMapPointMake(MKMapRectGetMidX(mRect), MKMapRectGetMaxY(mRect));
self.currentDist = MKMetersBetweenMapPoints(northMapPoint, southMapPoint);
}
If i set the map region to 1500 meters i get something like 1800 as a result..
Thanks for your help,
Vincent
Actually it was a really stupid mistake, if i do the same operation along the X axis then i get the correct value :
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
MKMapRect mRect = self.map.visibleMapRect;
MKMapPoint eastMapPoint = MKMapPointMake(MKMapRectGetMinX(mRect), MKMapRectGetMidY(mRect));
MKMapPoint westMapPoint = MKMapPointMake(MKMapRectGetMaxX(mRect), MKMapRectGetMidY(mRect));
self.currentDist = MKMetersBetweenMapPoints(eastMapPoint, westMapPoint);
}
- (void)mapView:(MKMapView *)map regionDidChangeAnimated:(BOOL)animated {
MKCoordinateSpan span = mapView.region.span;
NSLog(#" 1 = ~111 km -> %f = ~ %f km ",span.latitudeDelta,span.latitudeDelta*111);
}
According to the docs http://developer.apple.com/library/ios/#documentation/MapKit/Reference/MapKitDataTypesReference/Reference/reference.html,
latitudeDelta
The amount of north-to-south distance (measured in degrees) to display on the map. Unlike longitudinal distances, which vary based on the latitude, one degree of latitude is always approximately 111 kilometers (69 miles).
Thanks for the post all. I have an app that required a mile radius to figure out how many location records to fetch so this came in handy. Here is the swift equivalent for anyone who might come across this in the future.
let mRect: MKMapRect = self.mapView.visibleMapRect
let eastMapPoint = MKMapPointMake(MKMapRectGetMinX(mRect), MKMapRectGetMidY(mRect))
let westMapPoint = MKMapPointMake(MKMapRectGetMaxX(mRect), MKMapRectGetMidY(mRect))
let currentDistWideInMeters = MKMetersBetweenMapPoints(eastMapPoint, westMapPoint)
let milesWide = currentDistWideInMeters / 1609.34 // number of meters in a mile
println(milesWide)
Swift 4.2
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
let mapRect = mapView.visibleMapRect
let westMapPoint = MKMapPoint(x: mapRect.minX, y: mapRect.midY)
let eastMapPoint = MKMapPoint(x: mapRect.maxX, y: mapRect.midY)
let visibleDistance = westMapPoint.distance(to: eastMapPoint)
}
Here's an easier way (to get width and height in meters)...
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
MKMapRect rect = mapView.visibleMapRect;
double mapWidth = MKMapRectGetWidth(rect) / 10;
double mapHeight = MKMapRectGetHeight(rect) / 10;
}
Swift 4
extension MKMapView {
func regionInMeter() -> CLLocationDistance {
let eastMapPoint = MKMapPointMake(MKMapRectGetMinX(visibleMapRect), MKMapRectGetMidY(visibleMapRect))
let westMapPoint = MKMapPointMake(MKMapRectGetMaxX(visibleMapRect), MKMapRectGetMidY(visibleMapRect))
return MKMetersBetweenMapPoints(eastMapPoint, westMapPoint)
}
}