mapkit zoom to maximum scale objective c - ios

I am using mapkit .I have developed simple storyboard application .
1-The mapkit should zoom to maximum scale to show user location on loading mapkit.
2-On clicking loc.png the map should load the description of location with title and subtitle and detail about location
- (nullable MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation
{
MKAnnotationView * annotationView = [mapView dequeueReusableAnnotationViewWithIdentifier:#"testAnnotationView"];
if(annotationView == nil){
annotationView = [[MKAnnotationView alloc]initWithAnnotation:annotation reuseIdentifier:#"testAnnotationView"];
annotationView.image = [UIImage imageNamed:#"loc.png"];
annotationView.canShowCallout = true;
}
return annotationView;
}
How i can accomplish these task?From this link you can download sample project.https://drive.google.com/file/d/0B5pNDpbvZ8SnRExkamtmdkwzeWc/view?usp=sharing

Use this extension of mapKit, and adjust the values as you need, if the values is lower the the zoom is greater
EDITED
OBJECTIVE-C
.h
#import <MapKit/MapKit.h>
#interface MKMapView (Zoom)
-(void)zoomToUserLocation;
-(void)zoomToUserLocationWith:(CLLocationCoordinate2D)coordinate and:(CLLocationDistance)latitudinalMeters and:(CLLocationDistance)longitudinalMeters;
-(void)zoomToUserLocationWith:(CLLocationDistance)latitudinalMeters and:(CLLocationDistance)longitudinalMeters;
#end
.m
#import "MKMapView+Zoom.h"
#implementation MKMapView (Zoom)
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
// Drawing code
}
*/
-(void)zoomToUserLocation
{
[self zoomToUserLocationWith:1000 and:1000];
}
-(void)zoomToUserLocationWith:(CLLocationCoordinate2D)coordinate and:(CLLocationDistance)latitudinalMeters and:(CLLocationDistance)longitudinalMeters
{
[self setRegion:MKCoordinateRegionMakeWithDistance(coordinate, latitudinalMeters, longitudinalMeters)];
}
-(void)zoomToUserLocationWith:(CLLocationDistance)latitudinalMeters and:(CLLocationDistance)longitudinalMeters
{
if(self.userLocation.location != nil){
[self zoomToUserLocationWith:self.userLocation.location.coordinate and:latitudinalMeters and:longitudinalMeters];
}
}
#end
use it
[self.mapView zoomToUserLocation];
or
[self.mapView zoomToUserLocationWith:50 and:50];
or you can use it in
- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view{
[mapView zoomToUserLocationWith:view.annotation.coordinate and:500 and:500];
}
SWIFT
extension MKMapView {
func zoomToUserLocation() {
self.zoomToUserLocation(latitudinalMeters: 1000, longitudinalMeters: 1000)
}
func zoomToUserLocation(latitudinalMeters:CLLocationDistance,longitudinalMeters:CLLocationDistance)
{
guard let coordinate = userLocation.location?.coordinate else { return }
let region = MKCoordinateRegionMakeWithDistance(coordinate, latitudinalMeters, longitudinalMeters)
setRegion(region, animated: true)
}
}
Use it
mapView.zoomToUserLocation()
or
mapView.zoomToUserLocation(latitudinalMeters:50,longitudinalMeters:50)
Hope this helps

Here's code I wrote for zooming an WKInterfaceMap (WatchKit) in and out no less than its minimum (0.0) and no greater than its maximum (the width/height of the world map)—and, that, on an curve that accelerates the zoom increase the farther out you go and decelerates the zoom decrease the closer in you go:
- (void)crownDidRotate:(WKCrownSequencer *)crownSequencer rotationalDelta:(double)rotationalDelta
{
span.latitudeDelta += ((rotationalDelta * rotationalDelta) * (rotationalDelta)) + (span.latitudeDelta * rotationalDelta);
span.longitudeDelta += ((rotationalDelta * rotationalDelta) * (rotationalDelta)) + (span.longitudeDelta * rotationalDelta);
span.latitudeDelta = (span.latitudeDelta < 0) ? 0 : (span.latitudeDelta > MKCoordinateRegionForMapRect(MKMapRectWorld).span.latitudeDelta) ? MKCoordinateRegionForMapRect(MKMapRectWorld).span.latitudeDelta : span.latitudeDelta;
span.longitudeDelta = (span.longitudeDelta < 0) ? 0 : (span.longitudeDelta > MKCoordinateRegionForMapRect(MKMapRectWorld).span.longitudeDelta) ? MKCoordinateRegionForMapRect(MKMapRectWorld).span.longitudeDelta : span.longitudeDelta;
MKCoordinateRegion visibleRegion = MKCoordinateRegionMake(PlanetaryHourDataSource.sharedDataSource.locationManager.location.coordinate, span);
[self.map setRegion:visibleRegion];
}
Not only does this prevent invalid regions (and indicates to the user when the maximum and minimum zoom levels are reached), but it makes sure that the zoom slows down when the map shows more detail, and vice versa:
<iframe src="https://player.vimeo.com/video/313729712" width="640" height="1384" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
The acceleration curve looks like this:
The above answers provide arbitrary values, which will not necessarily correspond to either end of the spectrum.

Related

How can I display a UILabel on MKMapView at a specific zoom level?

I have a label that I would like to display on my map view, but the label should only display if the user zooms to a specific zoom level. So I'd like to do the following:
if (mapView.camera.altitude >= 5) {
//display label here
}
I like it to check and update the zoom level every time the user zooms. So I was thinking that ViewDidAppear would be the best place for this block of code.
Thanks in advance.
You should handle it via below code.
-(void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated
{
NSUInteger zoomLevel = MAXIMUM_ZOOM; // MAXIMUM_ZOOM is 20 with MapKit
MKZoomScale zoomScale = mapView.visibleMapRect.size.width / mapView.frame.size.width; //MKZoomScale is just a CGFloat typedef
double zoomExponent = log2(zoomScale);
zoomLevel = (NSUInteger)(MAXIMUM_ZOOM - ceil(zoomExponent));
if(zoomLevel > 5 && labelNotAdded)
{
//Add the label
}
else
{
//Remove the label
}
}

MKMapView how to change the User Nearest location pin image?

I want to change user nearest location pin image in map view.
"In my Project i show the some of the shop locations in map view. The locations (lat,long) are get from the api. Here i changed the given location pin image. it works fine. but i need to change the user nearest location pin image in the map view. I already get the distance details from the user current location to given api locations in that which are the locations are below 5 miles that location pin images are need to change. "
Here is my Annotation Code:
// View for Annotation Delegate Code for changing the pin image.
-(MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation: (id<MKAnnotation>)annotation
{
[self.annotationCustom_View removeFromSuperview];
[self.annotationCurrentLoc_View removeFromSuperview];
static NSString *identifier = #"myAnnotation";
CustomMapViewAnnotation * annotationView = (CustomMapViewAnnotation *)[self.locationsMap_View dequeueReusableAnnotationViewWithIdentifier:identifier];
if (!annotationView)
{
annotationView = [[CustomMapViewAnnotation alloc] initWithAnnotation:annotation reuseIdentifier:identifier];
if([annotation isKindOfClass:[MKUserLocation class]])
{
annotationView.image = [UIImage imageNamed:#"LocationsYour-Current-Location-Icon"]; // User Current Location Image
}
else
{
dispatch_async(dispatch_get_main_queue(), ^{
for(int i=0;i<[locations_ArrayList count];i++)
{
MapViewLocationModel *objValue=[locations_ArrayList objectAtIndex:i];
float value = [objValue.kiosk_distance floatValue];
if(value < 5.0)
{
annotationView.image = [UIImage imageNamed:#"LocationsFridge-Location-Icon"]; // Change the pin image which are the below 5.0 miles distance from the user current locations
}
else
{
annotationView.image = [UIImage imageNamed:#"LocationsBlackDot"]; // given api locations pin images
}
}
});
}
}
annotationView.canShowCallout = NO;
return annotationView;
}
This is my code. any one can help me on this?
Try following:
In order to display the pins in certain map area, first you need to
get the all pins inside the 5 miles.
// 1. Set the map zoom area visible of 5 miles:
mapView.region = MKCoordinateRegionMakeWithDistance(
centerCoordinate,
1609.344f * miles (5 in your case),
1609.344f * miles (5 in your case)
);
// 2. Now get the Rect of this map area:
MKMapRect mRect = self.map.visibleMapRect;
// 3. Get the all pins inside this Rect:
NSSet *annotationSet = [myMapView annotationsInMapRect:mRect];
// print number of annotations
NSLog(#"Number of annotations in rect: %d", annotationSet.count);
// this will return an array from the NSSet
NSArray *annotationArray = [annotationSet allObjects];
// 4. Assign some parameter to this annotation, by taking some property in the annotation class.
// 5. Now in your MapView Delegate method viewForAnnotation check the parameter and do the need full with the respective pins.
Hope this will help you to achieve what you want.

Set Minimum Zoom Level For MKMapView in iOS

I want to set minimum and maximum Pinch-In and Pinch-Out Zoomming Level For MKMapview. so user not Pinch after specific Level Of MKMapview Zooming.Currently I am using MKMapView+ZoomLevel Category Class to restrict Zoom-In and Out effect of MapView.
Code:-
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
if([self.mapView zoomLevel]<17) {
CLLocationCoordinate2D centerCoord = {latitude, longitude};
[self.mapView setCenterCoordinate:centerCoord zoomLevel:17 animated:NO];
}
}

Display MKMapViewAnnotations within a map view's visible rect

I'm displaying an MKMapView inside a Path-style parallax table view header. To create the effect, the mapView bounds is larger than the area visible to the user. I need to set the map view region such that all the map's annotations are contained within the visible rect of MKMapView. What's the best way to do this?
Edit for clarity: Here's a use-case. The mapView size is 320 x 380. The visible area, however, is defined by the rect (0.0, 20.0, 320.0, 100.0). I need to set the region such that all the annotations appear in this rect within the mapView.
Setting the map region so that all annotations are contained in a certain part of an MKMapView can be done in three steps. Input are the mapView and the annotationsFrame.
Calculate an MKMapRect mapRect that contains all annotations.
Calculate the padding insets from mapView.bounds and annotationsFrame.
Call -setVisibleMapRect:edgePadding:animated: on the map view.
Below is a screen shot of a test. The red overlay shows the annotationsFrame.
Here is the code. Beware: It's all in one method to simplify adding it to your code, and it is not tested for edge cases like passing in n annotations with the same coordinate, or having the annotations so far apart that the map would have to get zoomed out too much, or having coordinates that span the edge of the map at +/-180 degrees longitude.
- (void)zoomAnnotationsOnMapView:(MKMapView *)mapView toFrame:(CGRect)annotationsFrame animated:(BOOL)animated
{
if (_mapView.annotations.count < 2) return;
// Step 1: make an MKMapRect that contains all the annotations
NSArray *annotations = _mapView.annotations;
id <MKAnnotation> firstAnnotation = [annotations objectAtIndex:0];
MKMapPoint minPoint = MKMapPointForCoordinate(firstAnnotation.coordinate);
MKMapPoint maxPoint = minPoint;
for (id <MKAnnotation> annotation in annotations) {
MKMapPoint point = MKMapPointForCoordinate(annotation.coordinate);
if (point.x < minPoint.x) minPoint.x = point.x;
if (point.y < minPoint.y) minPoint.y = point.y;
if (point.x > maxPoint.x) maxPoint.x = point.x;
if (point.y > maxPoint.y) maxPoint.y = point.y;
}
MKMapRect mapRect = MKMapRectMake(minPoint.x, minPoint.y, maxPoint.x - minPoint.x, maxPoint.y - minPoint.y);
// Step 2: Calculate the edge padding
UIEdgeInsets edgePadding = UIEdgeInsetsMake(
CGRectGetMinY(annotationsFrame),
CGRectGetMinX(annotationsFrame),
CGRectGetMaxY(mapBounds) - CGRectGetMaxY(annotationsFrame),
CGRectGetMaxX(mapBounds) - CGRectGetMaxX(annotationsFrame)
);
// Step 3: Set the map rect
[mapView setVisibleMapRect:mapRect edgePadding:edgePadding animated:animated];
}
If you go for a perfect placement (and who doesn't), here are three things to consider:
The code assures that all the coordinates are in the annotationsFrame, but the annotations themselves may be outside. To prevent that, simply use more padding. For example, if your annotations are 20x20 and centered on the coordinate, use 10 more padding on all sides.
Below iOS 7, the map was not zooming to the perfect zoom scale, but to the next tile size (power of two). So there will be more space around the annotations than needed, just as shown on the screenshot.
On iOS 7, the map view will not only zoom perfectly, but automatically care about the status bar. To make the calculation correct, you need to subtract the status bar height from the top padding on iOS 7.
Starting from iOS 7.0, this can be easily achieved with showAnnotations.
Swift:
mapView.showAnnotations(mapView.annotations, animated: true)
Objective-C:
[mapView showAnnotations:mapView.annotations animated:YES];
The above statement will adjust the map view's visible rect in order to display all annotations.
You first need to add the annotations:
(of course this is after you already have a list of annotations)
Swift4:
self.mapView.addAnnotations(annotations)
let currentView = mapView.visibleMapRect
mapView.annotations(in: currentView)
You can use the currentView constant or directly place the MKMapRect as such: Below: (.visibleMapRect returns:
"The area currently displayed by the map view."
mapView.annotations(in: mapView.visibleMapRect)
I found an easier way without calculating is let the map view calculate it, then we adjust the edges.
//1: Show all annotation on the map view, but without animation
self.mapView.showAnnotations(self.mapView.annotations, animated: false)
//2: Get the current visible map rect
let currentMapRect = self.mapView.visibleMapRect
//3: Create the edges inset that you want the map view to show all annotation within
let padding = UIEdgeInsets(top: 100, left: 100, bottom: 100, right: 100)
//4: Set current map rect but with new padding, also set animation to true to see effect
self.mapView.setVisibleMapRect(currentMapRect, edgePadding: padding, animated: true)
If you're prepared to approximate the calculations you can do it using some clever scaling.
Your target area is 80 tall out of a mapView that is 380. Therefore you want a region that is 4.75x taller than the region calculated to fit your annotations. (0.25 extra above and 3.5 extra below).
First you need to get a region (or maprect, what ever you prefer working in) and make it the same proportions as your target viewable area. This is because a really wide and short region would not be touching the top and bottom of the viewable area and therefore multiplying its height would not make something that touched the top and bottom of your map view. So if viewable_height/viewable_width > annotations_height/annotations_width you should set the annotations_height to annotations_width * (viewable_height/viewable_width).
With that you then add 25% on to the north of the annotations box and 350% on to the south. You can do this by moving the center 212.5% (of the current height) south and increasing the vertical span by 475%.
Now, all of this is an approximation given that the world is sphere and we're not looking at a planar projection (i.e. 1 degree of latitude near the equator is drawn smaller than 1 degree near the poles). But if you wally want to be accurate you could look into scaling the numbers according to latitude and such. If you're only dealing with annotations on a city-sized scale you'll probably be ok.
Hope that helps.
if you want to find the annotations that are in a given rect:
- (NSArray*)allAnnotationsInMapRect:(MKMapRect)mapRect {
NSMutableArray *annotationsInRect = [NSMutableArray array];
for(id<MKAnnotation *ann in self.allAnnotations) {
MKMapPoint pt = MKMapPointForCoordinate(ann.coordinate);
if(MKMapRectContainsPoint(mapRect, pt)) {
[annotationsInRect addObject:ann];
}
}
return annotationsInRect;
}
and to assure the annotation VIEWS are in the rect, get the region for the annotations,
then walk through them and get each view's bounds see if the bounds fit inside the visibleRect of the map and if not modify the region!
~~ like this:
- (void)assureAnnotationViewsAreVisible:(NSArray*)annotations originalRegion:(MKCoordinateRegion)originalRegion {
CGFloat smallestX = MAXFLOAT;
CGFloat smallestY = MAXFLOAT;
CGFloat biggestX = -100;
CGFloat biggestY = -100;
//NSLog(#"---: %d", annotations.count);
for(id<MKAnnotation> *annotation in annotations) {
UIView *annotationView = [self.mapView viewForAnnotation:v];
CGRect annotationViewFrame = annotationView.bounds;
annotationViewFrame.origin = [self.mapView convertCoordinate:annotationView.coordinate toPointToView:self.mapView];
annotationViewFrame.origin = CGPointMake(annotationViewFrame.origin.x-annotationViewFrame.size.width/2,
annotationViewFrame.origin.y-annotationViewFrame.size.height);
smallestX = MIN(annotationViewFrame.origin.x, smallestX);
smallestY = MIN(annotationViewFrame.origin.y, smallestY);
biggestX = MAX(annotationViewFrame.origin.x+annotationViewFrame.size.width, biggestX);
biggestY = MAX(annotationViewFrame.origin.y+annotationViewFrame.size.height, biggestY);
}
//NSLog(#"---");
CGRect bounds = self.mapView.bounds;
if(smallestX < bounds.origin.x || smallestY < bounds.origin.y || biggestX > bounds.origin.x+bounds.size.width || biggestY > bounds.origin.y+bounds.size.height) {
CGRect neededRect = bounds;
neededRect.origin = CGPointMake(MIN(bounds.origin.x, smallestX), MIN(bounds.origin.y, smallestY));
neededRect.size = CGSizeMake(MAX(bounds.size.width, biggestX), MAX(bounds.size.height, biggestY));
MKCoordinateRegion neededRegion = [self.mapView convertRect:neededRect toRegionFromView:self.mapView];
_ignoreRegionChange = YES;
[self.mapView setRegion:originalRegion animated:NO];
_ignoreRegionChange = NO;
[self.mapView setRegion:neededRegion animated:YES];
}
else {
MKCoordinateRegion currentRegion = self.mapView.region;
_ignoreRegionChange = YES;
[self.mapView setRegion:originalRegion animated:NO];
_ignoreRegionChange = NO;
[self.mapView setRegion:currentRegion animated:YES];
}
}
Try to get from all your annotation edges value (max and min) for lan and lon.
Define this value on the beginning:
static float maxLat = FLT_MIN;
static float maxLon = FLT_MIN;
static float minLat = FLT_MAX;
static float minLon = FLT_MAX;
and then use this function to calculate span and region:
- (void) zoomAndFit {
for(int i = 0; i < [self.points count]; i++) {
PRPlaceModel *place = [self.points objectAtIndex:i];
CLLocationCoordinate2D location;
location.latitude = [place.lat floatValue];
location.longitude = [place.lon floatValue];
minLat = MIN(minLat, location.latitude);
minLon = MIN(minLon, location.longitude);
maxLat = MAX(maxLat, location.latitude);
maxLon = MAX(maxLon, location.longitude);
}
MKCoordinateRegion region;
MKCoordinateSpan span;
span.latitudeDelta = 1.2*(maxLat - minLat);
span.longitudeDelta = 1.2*(maxLon - minLon);
CLLocationCoordinate2D location;
location.latitude = (minLat + maxLat)/2;
location.longitude = (minLon + maxLon)/2;
region.span=span;
region.center=location;
[self.mapView setRegion:region animated:TRUE];
[self.mapView regionThatFits:region];
}
And use it in viewDidLoad method:
-(void)viewDidLoad
{
[super viewDidLoad];
[self zoomAndFit];
}

How to adjust region to fit custom annotation callout that have just appeared?

I use my custom subclass of MKAnnotationView. In mapView:didSelectAnnotationView: method of my Map's delegate I call the method of this class, which adds UIImageView with an image as a subview - it serves as my custom annotation callout.
When using default MKPinAnnotationView map does automatically adjust map region to display the annotation callout that have just appeared. How can I implement this behavior using custom MKAnnotationView subclass?
Current solution
I've crafted demo project having the stuff discussed below implemented: see there
AdjustRegionToFitAnnotationCallout project.
The latest iOS7 changes in how Map Kit's MKMapView renders map annotations made me to revisit this problem. I've made more accurate thinking about it and come up with much, very much better solution. I will leave the previous solution at the bottom of this answer, but remember - I was so wrong when I did it that way.
First of all we will need a helper CGRectTransformToContainRect() that expands a given CGRect to contain another CGRect.
Note: it's behavior is different from what CGRectUnion() does - CGRectUnion() returns just the smallest CGRect containing both CGRects, whereas the following helper allows parallel movement i.e. CGRectTransformToContainRect(CGRectMake(0, 0, 100, 100), CGRectMake(50, 50, 100, 100)) equals (CGRect){50, 50, 100, 100} and not (CGRect){0, 0, 150, 150} like CGRectUnion() does it. This behavior is exactly what we need when we want to have only adjusts using parallel movements and want to avoid map's zooming.
static inline CGRect CGRectTransformToContainRect(CGRect rectToTransform, CGRect rectToContain) {
CGFloat diff;
CGRect transformedRect = rectToTransform;
// Transformed rect dimensions should encompass the dimensions of both rects
transformedRect.size.width = MAX(CGRectGetWidth(rectToTransform), CGRectGetWidth(rectToContain));
transformedRect.size.height = MAX(CGRectGetHeight(rectToTransform), CGRectGetHeight(rectToContain));
// Comparing max X borders of both rects, adjust if
if ((diff = CGRectGetMaxX(rectToContain) - CGRectGetMaxX(transformedRect)) > 0) {
transformedRect.origin.x += diff;
}
// Comparing min X borders of both rects, adjust if
else if ((diff = CGRectGetMinX(transformedRect) - CGRectGetMinX(rectToContain)) > 0) {
transformedRect.origin.x -= diff;
}
// Comparing max Y borders of both rects, adjust if
if ((diff = CGRectGetMaxY(rectToContain) - CGRectGetMaxY(transformedRect)) > 0) {
transformedRect.origin.y += diff;
}
// Comparing min Y borders of both rects, adjust if
else if ((diff = CGRectGetMinY(transformedRect) - CGRectGetMinY(rectToContain)) > 0) {
transformedRect.origin.y -= diff;
}
return transformedRect;
}
Adjust method wrapped into an Objective-C category MKMapView(Extensions):
#implementation MKMapView (Extensions)
- (void)adjustToContainRect:(CGRect)rect usingReferenceView:(UIView *)referenceView animated:(BOOL)animated {
// I just like this assert here
NSParameterAssert(referenceView);
CGRect visibleRect = [self convertRegion:self.region toRectToView:self];
// We convert our annotation from its own coordinate system to a coodinate system of a map's top view, so we can compare it with the bounds of the map itself
CGRect annotationRect = [self convertRect:rect fromView:referenceView.superview];
// Fatten the area occupied by your annotation if you want to have a margin after adjustment
CGFloat additionalMargin = 2;
adjustedRect.origin.x -= additionalMargin;
adjustedRect.origin.y -= additionalMargin;
adjustedRect.size.width += additionalMargin * 2;
adjustedRect.size.height += additionalMargin * 2;
// This is the magic: if the map must expand its bounds to contain annotation, it will do this
CGRect adjustedRect = CGRectTransformToContainRect(visibleRect, annotationRect);
// Now we just convert adjusted rect to a coordinate region
MKCoordinateRegion adjustedRegion = [self convertRect:adjustedRect toRegionFromView:self];
// Trivial regionThatFits: sugar and final setRegion:animated: call
[self setRegion:[self regionThatFits:adjustedRegion] animated:animated];
}
#end
Now the controller and views:
#interface AnnotationView : MKAnnotationView
#property AnnotationCalloutView *calloutView;
#property (readonly) CGRect annotationViewWithCalloutViewFrame;
#end
#implementation AnnotationView
- (void)showCalloutBubble {
// This is a code where you create your custom annotation callout view
// add add it using -[self addSubview:]
// At the end of this method a callout view should be displayed.
}
- (CGRect)annotationViewWithCalloutViewFrame {
// Here you should adjust your annotation frame so it match itself in the moment when annotation callout is displayed and ...
return CGRectOfAdjustedAnnotation; // ...
}
#end
When AnnotationView-classed annotation is selected on map, it adds its calloutView as a subview, so custom annotation callout view is displayed. It is done using MKMapViewDelegate's method:
- (void)mapView:(MapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view {
// AnnotationPresenter is just a class that contains information to be displayed on callout annotation view
if ([view.annotation isKindOfClass:[AnnotationPresenter class]]) {
// Hide another annotation if it is shown
if (mapView.selectedAnnotationView != nil && [mapView.selectedAnnotationView isKindOfClass:[AnnotationView class]] && mapView.selectedAnnotationView != view) {
[mapView.selectedAnnotationView hideCalloutBubble];
}
mapView.selectedAnnotationView = view;
annotationView *annotationView = (annotationView *)view;
// This just adds *calloutView* as a subview
[annotationView showCalloutBubble];
[mapView adjustToContainRect:annotationView.annotationViewWithCalloutViewFrame usingReferenceView:annotationView animated:NO];
}
}
Of course your implementation may be different from what I've described here (mine is!). The most important part of above code is of course the [MKMapView adjustToContainRect:usingReferenceView:animated: method. Now I am really satisfied with the current solution and my understanding of this (and some related) problem. If you need any comments about the solution above, feel free to contact me (see profile).
The following Apple docs are very useful to understand what is going on in methods like -[MKMapView convertRect:fromView:]:
http://developer.apple.com/library/ios/#documentation/MapKit/Reference/MKMapView_Class/MKMapView/MKMapView.html
http://developer.apple.com/library/ios/#documentation/MapKit/Reference/MapKitDataTypesReference/Reference/reference.html
http://developer.apple.com/library/ios/#documentation/MapKit/Reference/MapKitFunctionsReference/Reference/reference.html
Also the first 10-15 minutes of WWDC 2013 session "What’s New in Map Kit" (#304) are very good to watch to have an excellent quick demo of the whole "Map with annotations" setup done by Apple engineer.
Initial solution (Does not work in iOS7, do not use it, use the solution above instead)
Somehow I forgot to answer my question at a time. Here is the complete solution I use nowadays (edited slightly for readability):
First of all a bit of map logic to be encapsulated somewhere in helpers file like MapKit+Helpers.h
typedef struct {
CLLocationDegrees top;
CLLocationDegrees bottom;
} MKLatitudeEdgedSpan;
typedef struct {
CLLocationDegrees left;
CLLocationDegrees right;
} MKLongitudeEdgedSpan;
typedef struct {
MKLatitudeEdgedSpan latitude;
MKLongitudeEdgedSpan longitude;
} MKEdgedRegion;
MKEdgedRegion MKEdgedRegionFromCoordinateRegion(MKCoordinateRegion region) {
MKEdgedRegion edgedRegion;
float latitude = region.center.latitude;
float longitude = region.center.longitude;
float latitudeDelta = region.span.latitudeDelta;
float longitudeDelta = region.span.longitudeDelta;
edgedRegion.longitude.left = longitude - longitudeDelta / 2;
edgedRegion.longitude.right = longitude + longitudeDelta / 2;
edgedRegion.latitude.top = latitude + latitudeDelta / 2;
edgedRegion.latitude.bottom = latitude - latitudeDelta / 2;
return edgedRegion;
}
Like MKCoordinateRegion (center coordinate + spans), MKEdgedRegion is just a way to define a region but using coordinates of its edges instead.
MKEdgedRegionFromCoordinateRegion() is a self-explanatory converter-method.
Suppose we have the following class for our annotations, containing its callout as a subview.
#interface AnnotationView : MKAnnotationView
#property AnnotationCalloutView *calloutView;
#end
When AnnotationView-classed annotation is selected on map, it adds its calloutView as a subview, so custom annotation callout view is displayed. It is done using MKMapViewDelegate's method:
- (void)mapView:(MapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view {
// AnnotationPresenter is just a class that contains information to be displayed on callout annotation view
if ([view.annotation isKindOfClass:[AnnotationPresenter class]]) {
// Hide another annotation if it is shown
if (mapView.selectedAnnotationView != nil && [mapView.selectedAnnotationView isKindOfClass:[AnnotationView class]] && mapView.selectedAnnotationView != view) {
[mapView.selectedAnnotationView hideCalloutBubble];
}
mapView.selectedAnnotationView = view;
annotationView *annotationView = (annotationView *)view;
// This just adds *calloutView* as a subview
[annotationView showCalloutBubble];
/* Here the trickiest piece of code goes */
/* 1. We capture _annotation's (not callout's)_ frame in its superview's (map's!) coordinate system resulting in something like (CGRect){4910547.000000, 2967852.000000, 23.000000, 28.000000} The .origin.x and .origin.y are especially important! */
CGRect annotationFrame = annotationView.frame;
/* 2. Now we need to perform an adjustment, so our frame would correspond to the annotation view's _callout view subview_ that it holds. */
annotationFrame.origin.x = annotationFrame.origin.x + ANNOTATION_CALLOUT_TRIANLE_HALF; // Mine callout view has small x offset - you should choose yours!
annotationFrame.origin.y = annotationFrame.origin.y - ANNOTATION_CALLOUT_HEIGHT / 2; // Again my custom offset.
annotationFrame.size = placeAnnotationView.calloutView.frame.size; // We can grab calloutView size directly because in its case we don't care about the coordinate system.
MKCoordinateRegion mapRegion = mapView.region;
/* 3. This was a long run before I did stop to try to pass mapView.view as an argument to _toRegionFromView_. */
/* annotationView.superView is very important - it gives us the same coordinate system that annotationFrame.origin is based. */
MKCoordinateRegion annotationRegion = [mapView convertRect:annotationFrame toRegionFromView:annotationView.superview];
/* I hope that the following MKEdgedRegion magic is self-explanatory */
MKEdgedRegion mapEdgedRegion = MKEdgedRegionFromCoordinateRegion(mapRegion);
MKEdgedRegion annotationEdgedRegion = MKEdgedRegionFromCoordinateRegion(annotationRegion);
float diff;
if ((diff = (annotationEdgedRegion.longitude.left - mapEdgedRegion.longitude.left)) < 0 ||
(diff = (annotationEdgedRegion.longitude.right - mapEdgedRegion.longitude.right)) > 0)
mapRegion.center.longitude += diff;
if ((diff = (annotationEdgedRegion.latitude.bottom - mapEdgedRegion.latitude.bottom)) < 0 ||
(diff = (annotationEdgedRegion.latitude.top - mapEdgedRegion.latitude.top)) > 0)
mapRegion.center.latitude += diff;
mapView.region = mapRegion;
}
}
I was looking for a similar solution, to fit a route and a callout in the visible rectangle of the screen. I tried some solutions but finally ended up just with setting enough padding on setVisibleMapRect:edgePadding:animated:. May not be as sophisticated, but basically does what I needed.
MKMapRect routeMapRect = myRoute.polyline.boundingMapRect;
CGFloat padding = myCallout.bounds.width / 2.0;
[myMapView setVisibleMapRect: routeMapRect edgePadding:UIEdgeInsetsMake(padding, padding, padding, padding) animated:YES];
Of course this can be way more optimized, e.g. with detecting on which side you actually need the padding and setting a smaller one on the other sides. But you get the idea.

Resources