MKPolygon ontouch detailview - iphone

I've created a MKMapView with MKPolygons based on coordinates. There are multiple polygons on the map (look here for an example of what I am re-creating as an app).
What I am trying to do is when the user touches the polygon, it opens a popover view with information about the location. This information is currently stored inside a plist file with the coordinates.
What I currently have so far is that I am able to get touch event and print to the log that the polygon was touched.
The question that I have is:
Can MKPolygonView be used like an MKAnnotationView where once the user taps the pin more information pops up about that current location?
I want to do the same for the polygon view. When touched, the user would see more information about the location that is stored in the plist. If it is possible what would be the best way to get it to work?
My current code is below.
#import "outagemapViewController.h"
#import "MyAnnotation.h"
#import "WildcardGestureRecognizer.h"
#define METERS_PER_MILE 46309.344
#interface outagemapViewController ()
#end
#implementation outagemapViewController
- (void)viewDidLoad {
outages = [[NSArray alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"outages"ofType:#"plist"]];
for (NSDictionary *coloredAreas in outages) {
coordinateData = coloredAreas[#"coords"];
test = coloredAreas[#"outages"];
NSLog(#"test %#", test);
coordsLen = [coordinateData count];
NSLog(#"coords %d", coordsLen);
CLLocationCoordinate2D coords[coordsLen];
for (i=0; i < coordsLen; i++) {
NSString *lat = coordinateData[i];
NSArray *latt = [lat componentsSeparatedByString:#","];
double latitude = [[latt objectAtIndex:0] doubleValue];
double longitude = [[latt objectAtIndex:1] doubleValue];
coords[i] = CLLocationCoordinate2DMake(latitude, longitude);
}
MKPolygon* poly2 = [MKPolygon polygonWithCoordinates:coords count:coordsLen];
poly2.title=#"test";
[self.mapView addOverlay:poly2];
}
}
- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id <MKOverlay>)overlay {
{
if ([overlay isKindOfClass:[MKPolygon class]])
{
MKPolygonView* aView = [[MKPolygonView alloc] initWithPolygon:(MKPolygon*)overlay];
int numbers = [test intValue];
if(numbers >= 10){
aView.fillColor = [[UIColor greenColor] colorWithAlphaComponent:0.6];
aView.strokeColor = [[UIColor greenColor] colorWithAlphaComponent:1.0];
aView.lineWidth = 3;
}else if(numbers < 10){
aView.fillColor = [[UIColor yellowColor] colorWithAlphaComponent:0.6];
aView.strokeColor = [[UIColor yellowColor] colorWithAlphaComponent:1.0];
aView.lineWidth = 3;
}
return aView;
}
return nil;
}
}
-(void)viewWillAppear:(BOOL)animated{
CLLocationCoordinate2D zoomLocation;
zoomLocation.latitude = 35.20418;
zoomLocation.longitude = -89.86862;
MKCoordinateRegion viewRegion = MKCoordinateRegionMakeWithDistance(zoomLocation, 0.5*METERS_PER_MILE, 0.5*METERS_PER_MILE);
[_mapView setRegion:viewRegion animated:YES];
WildcardGestureRecognizer * tapInterceptor = [[WildcardGestureRecognizer alloc] init];
tapInterceptor.touchesBeganCallback = ^(NSSet * touches, UIEvent * event) {
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self.mapView];
CLLocationCoordinate2D coord = [self.mapView convertPoint:point toCoordinateFromView:self.mapView];
MKMapPoint mapPoint = MKMapPointForCoordinate(coord);
for (id overlay in self.mapView.overlays)
{
if ([overlay isKindOfClass:[MKPolygon class]])
{
MKPolygon *poly = (MKPolygon*) overlay;
id view = [self.mapView viewForOverlay:poly];
if ([view isKindOfClass:[MKPolygonView class]])
{
MKPolygonView *polyView = (MKPolygonView*) view;
CGPoint polygonViewPoint = [polyView pointForMapPoint:mapPoint];
BOOL mapCoordinateIsInPolygon = CGPathContainsPoint(polyView.path, NULL, polygonViewPoint, NO);
if (mapCoordinateIsInPolygon) {
// debug(#"hit!");
NSLog(#"hit");
} else {
NSLog(#"miss");
}
}
}
}
};
[self.mapView addGestureRecognizer:tapInterceptor];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#end

Unfortunately, for overlays, there's no built-in touch-detection and callout view like there is for annotations.
You'll have to do the touch-detection manually like you're already doing (and it looks like it should work).
(Even more unfortunate here is that adding a gesture recognizer directly to the overlay view doesn't work -- you have to add it to the whole map and then check whether the touch point is in any overlay.)
For an overlay callout view, once you've detected a touch on an overlay, you can create a custom UIView and do addSubview. I suggest adding it to the map instead of the overlay view and you might be able to use the CGPoint point you are already calculating to determine the frame of the custom callout view.
You might also want to keep a ivar/property reference to the overlay callout view so it can be easily removed and re-added if the user taps on another overlay while the callout for another overlay is already displayed.
Another option which is probably easier is to create a custom UIViewController and present or push it. The specifics of showing it depend on whether you're using a navigation controller and/or storyboard.
If your app is also built for iPad, you could also show the "callout" using a UIPopoverController.
See How do I display a UIPopoverView as a annotation to the map view? (iPad) for a code example (it's with an annotation but you should be able to adapt it for the overlay).
Once you've identified which overlay was tapped, you need to display its associated data which is in your original data source (the outages array). Right now, overlays are created and added but have no reference back to the original data object (outage dictionary in outages array).
(Subclassing MKPolygon to add a custom property has issues and workarounds and creating a completely custom MKOverlay class introduces a lot of other additional work.)
For your current data source structure, a simple, quick (and somewhat crude) option is to set the overlay's title property to the index in the outages array of the outage object associated with the overlay. Since the title property is an NSString and the array index is an integer, we'll convert it to a string:
NSUInteger outageIndex = [outages indexOfObject:coloredAreas];
poly2.title = [NSString stringWithFormat:#"%d", outageIndex];
[self.mapView addOverlay:poly2];
In viewForOverlay, it looks like you're using test (which comes from an outage object) to determine the polygon's color. The value of the externally declared/set test variable will not necessarily be in sync with the overlay the delegate method is currently being called for (the map could call viewForOverlay multiple times for the same overlay and not necessarily in the order you add them). You have to retrieve the outage object based on some property of the overlay parameter. Since we are setting the overlay's title property to the outage's index:
//int numbers = [test intValue]; <-- remove this line
int outageIndex = [overlay.title intValue];
NSDictionary *outageDict = [outages objectAtIndex:outageIndex];
id outageNumbersObject = outageDict[#"outages"];
//replace id above with actual type
//can't tell from code in question whether it's NSString or NSNumber
int numbers = [outageNumbersObject intValue];
//use "numbers" to set polygon color...
Finally, when an overlay is tapped, you use the same method as in viewForOverlay to get the outage object:
if (mapCoordinateIsInPolygon) {
int outageIndex = [overlay.title intValue];
NSDictionary *outageDict = [outages objectAtIndex:outageIndex];
NSLog(#"hit, outageDict = %#", outageDict);
//show view with info from outageDict...
}

Related

Issue with drawing path over MapKit

I have created an application that constantly reads the current user coordinates and store them in a SQLite database.
I have a map that is displayed over the whole screen.
And now I want to draw a line over the map while the user moves.
I already created all this.
The problem is that I can't make it to be a 'live'. The Overlay is not updating.
This is the logic:
In ViewDidLoad I have
...
if (nil != self.routeLine) {
[self.mapView addOverlay:self.routeLine];
}
In a function that handle each new coordinates I have:
...
NSString* coordinate = [NSString stringWithFormat:#"%f,%f", thisLocation.longitude, thisLocation.latitude];
[self.paths addObject:coordinate];
MKMapPoint northEastPoint;
MKMapPoint southWestPoint;
// create a c array of points.
MKMapPoint* pointArr = malloc(sizeof(CLLocationCoordinate2D) * self.paths.count);
for(int idx = 0; idx < self.paths.count; idx++)
{
// break the string down even further to latitude and longitude fields.
NSString* currentPointString = [self.paths objectAtIndex:idx];
NSArray* latLonArr = [currentPointString componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:#","]];
CLLocationDegrees latitude = [[latLonArr objectAtIndex:1] doubleValue];
CLLocationDegrees longitude = [[latLonArr objectAtIndex:0] doubleValue];
// create our coordinate and add it to the correct spot in the array
CLLocationCoordinate2D coordinate = CLLocationCoordinate2DMake(latitude, longitude);
MKMapPoint point = MKMapPointForCoordinate(coordinate);
// adjust the bounding box
// if it is the first point, just use them, since we have nothing to compare to yet.
if (idx == 0) {
northEastPoint = point;
southWestPoint = point;
}
else
{
if (point.x > northEastPoint.x)
northEastPoint.x = point.x;
if(point.y > northEastPoint.y)
northEastPoint.y = point.y;
if (point.x < southWestPoint.x)
southWestPoint.x = point.x;
if (point.y < southWestPoint.y)
southWestPoint.y = point.y;
}
pointArr[idx] = point;
}
// create the polyline based on the array of points.
self.routeLine = [MKPolyline polylineWithPoints:pointArr count:self.paths.count];
_routeRect = MKMapRectMake(southWestPoint.x, southWestPoint.y, northEastPoint.x - southWestPoint.x, northEastPoint.y - southWestPoint.y);
// clear the memory allocated earlier for the points
free(pointArr);
This is viewForOverlay delegate function:
- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id <MKOverlay>)overlay
{
MKOverlayView* overlayView = nil;
if(overlay == self.routeLine)
{
//if we have not yet created an overlay view for this overlay, create it now.
if(nil == self.routeLineView)
{
self.routeLineView = [[MKPolylineView alloc] initWithPolyline:self.routeLine];
self.routeLineView.fillColor = [UIColor blueColor];
self.routeLineView.strokeColor = [UIColor blueColor];
self.routeLineView.lineWidth = 5;
}
overlayView = self.routeLineView;
}
return overlayView;
}
In viewDidLoad, the code calls addOverlay with self.routeLine which I assume is initially set to the previously-saved coordinates.
The map view adds the MKPoyline that routeLine points to into its internal list of overlays and draws the overlay.
Then, in the "function that handles each new coordinate", self.routeLine is changed to point to a new MKPolyline.
The reason the overlay view is not updated by the map is because the map view is still using the original MKPolyline that was passed to it when addOverlay was called.
Since MKPolyline itself is not mutable (it does not allow one to change the list of points/coordinates after creation), there are two main options:
Remove the existing overlay and add a new one with the updated coordinates. This is the simplest option. After setting self.routeLine to a new MKPolyline, do the following (for example):
[self.mapView removeOverlays:mapView.overlays];
self.routeLineView = nil;
[self.mapView addOverlay:self.routeLine];
The main drawback to this simple approach is that the overlay will seem to flicker if the updates are done frequently or if the overlay is large or complex.
The alternative is to create a custom overlay and overlay view that are mutable and enabling you to dynamically refresh the overlay more quickly and smoothly.
Fortunately, Apple has provided a sample app called Breadcrumb which does exactly that.
Maybe you just forgot to call [self.view setNeedsDisplay]?
UIView docs

iPhone MKMapView Annotation Clustering

I've got quite a lot of pins to put on my map so I think it would be a nice idea to cluster those annotations. I'm not really sure how to achieve this on iPhone, I was able to work something out with google maps and some javascript examples. But iPhone uses its mkmapview and I have no idea how to cluster annotations in there.
Any ideas or frameworks that you know and are good? Thanks.
You don't necessarily need to use a 3rd party framework because since iOS 4.2, MKMapView has a method called - (NSSet *)annotationsInMapRect:(MKMapRect)mapRect which you can use to do your clustering.
Check out the WWDC11 Session video 'Visualizing Information Geographically with MapKit'. About half way through it explains how to do it. But I'll summarize the concept for you:
Use Two maps (second map is never added to the view hierarchy)
Second map contains all annotations (again, it's never drawn)
Divide map area into a grid of squares
Use -annotationsInMapRect method to get annotation data from
invisible map
Visible map builds its annotations from this data from invisible map
Fortunately, you don't need 3rd party framework's anymore. iOS 11 has native clustering support.
You need to implement mapView:clusterAnnotationForMemberAnnotations: method.
Get more details in the Apple example: https://developer.apple.com/sample-code/wwdc/2017/MapKit-Sample.zip
Since this is a very common problem and i needed a solution i have wrote a custom subclass of MKMapView which supports clustering. Then i made it available open source! You can get it here: https://github.com/yinkou/OCMapView.
It manages the clustering of the annotations and you can handle their views by yourself.
You don't have to do anything but to copy the OCMapView folder to your project, create a MKMapView in your nib and set its class to OCMapView. (Or create and delegate it in code like a regular MKMapView)
By using Apple demo code it's easy to implement clustering concept in our code. Reference link
Simply we can use following code for the Clustering
Steps to implement clustering
Step1 : The important thing is for clustering we use two mapviews(allAnnotationsMapView, ), One is for reference(allAnnotationsMapView).
#property (nonatomic, strong) MKMapView *allAnnotationsMapView;
#property (nonatomic, strong) IBOutlet MKMapView *mapView;
In viewDidLoad
_allAnnotationsMapView = [[MKMapView alloc] initWithFrame:CGRectZero];
Step2 : Add all annotations to the _allAnnotationsMapView, In below _photos are the annotations array.
[_allAnnotationsMapView addAnnotations:_photos];
[self updateVisibleAnnotations];
Step3 : Add below methods for clustering, in this PhotoAnnotation is the custom annotation.
MapViewDelegate methods
- (void)mapView:(MKMapView *)aMapView regionDidChangeAnimated:(BOOL)animated {
[self updateVisibleAnnotations];
}
- (void)mapView:(MKMapView *)aMapView didAddAnnotationViews:(NSArray *)views {
for (MKAnnotationView *annotationView in views) {
if (![annotationView.annotation isKindOfClass:[PhotoAnnotation class]]) {
continue;
}
PhotoAnnotation *annotation = (PhotoAnnotation *)annotationView.annotation;
if (annotation.clusterAnnotation != nil) {
// animate the annotation from it's old container's coordinate, to its actual coordinate
CLLocationCoordinate2D actualCoordinate = annotation.coordinate;
CLLocationCoordinate2D containerCoordinate = annotation.clusterAnnotation.coordinate;
// since it's displayed on the map, it is no longer contained by another annotation,
// (We couldn't reset this in -updateVisibleAnnotations because we needed the reference to it here
// to get the containerCoordinate)
annotation.clusterAnnotation = nil;
annotation.coordinate = containerCoordinate;
[UIView animateWithDuration:0.3 animations:^{
annotation.coordinate = actualCoordinate;
}];
}
}
}
clustering Handling methods
- (id<MKAnnotation>)annotationInGrid:(MKMapRect)gridMapRect usingAnnotations:(NSSet *)annotations {
// first, see if one of the annotations we were already showing is in this mapRect
NSSet *visibleAnnotationsInBucket = [self.mapView annotationsInMapRect:gridMapRect];
NSSet *annotationsForGridSet = [annotations objectsPassingTest:^BOOL(id obj, BOOL *stop) {
BOOL returnValue = ([visibleAnnotationsInBucket containsObject:obj]);
if (returnValue)
{
*stop = YES;
}
return returnValue;
}];
if (annotationsForGridSet.count != 0) {
return [annotationsForGridSet anyObject];
}
// otherwise, sort the annotations based on their distance from the center of the grid square,
// then choose the one closest to the center to show
MKMapPoint centerMapPoint = MKMapPointMake(MKMapRectGetMidX(gridMapRect), MKMapRectGetMidY(gridMapRect));
NSArray *sortedAnnotations = [[annotations allObjects] sortedArrayUsingComparator:^(id obj1, id obj2) {
MKMapPoint mapPoint1 = MKMapPointForCoordinate(((id<MKAnnotation>)obj1).coordinate);
MKMapPoint mapPoint2 = MKMapPointForCoordinate(((id<MKAnnotation>)obj2).coordinate);
CLLocationDistance distance1 = MKMetersBetweenMapPoints(mapPoint1, centerMapPoint);
CLLocationDistance distance2 = MKMetersBetweenMapPoints(mapPoint2, centerMapPoint);
if (distance1 < distance2) {
return NSOrderedAscending;
} else if (distance1 > distance2) {
return NSOrderedDescending;
}
return NSOrderedSame;
}];
PhotoAnnotation *photoAnn = sortedAnnotations[0];
NSLog(#"lat long %f %f", photoAnn.coordinate.latitude, photoAnn.coordinate.longitude);
return sortedAnnotations[0];
}
- (void)updateVisibleAnnotations {
// This value to controls the number of off screen annotations are displayed.
// A bigger number means more annotations, less chance of seeing annotation views pop in but decreased performance.
// A smaller number means fewer annotations, more chance of seeing annotation views pop in but better performance.
static float marginFactor = 2.0;
// Adjust this roughly based on the dimensions of your annotations views.
// Bigger numbers more aggressively coalesce annotations (fewer annotations displayed but better performance).
// Numbers too small result in overlapping annotations views and too many annotations on screen.
static float bucketSize = 60.0;
// find all the annotations in the visible area + a wide margin to avoid popping annotation views in and out while panning the map.
MKMapRect visibleMapRect = [self.mapView visibleMapRect];
MKMapRect adjustedVisibleMapRect = MKMapRectInset(visibleMapRect, -marginFactor * visibleMapRect.size.width, -marginFactor * visibleMapRect.size.height);
// determine how wide each bucket will be, as a MKMapRect square
CLLocationCoordinate2D leftCoordinate = [self.mapView convertPoint:CGPointZero toCoordinateFromView:self.view];
CLLocationCoordinate2D rightCoordinate = [self.mapView convertPoint:CGPointMake(bucketSize, 0) toCoordinateFromView:self.view];
double gridSize = MKMapPointForCoordinate(rightCoordinate).x - MKMapPointForCoordinate(leftCoordinate).x;
MKMapRect gridMapRect = MKMapRectMake(0, 0, gridSize, gridSize);
// condense annotations, with a padding of two squares, around the visibleMapRect
double startX = floor(MKMapRectGetMinX(adjustedVisibleMapRect) / gridSize) * gridSize;
double startY = floor(MKMapRectGetMinY(adjustedVisibleMapRect) / gridSize) * gridSize;
double endX = floor(MKMapRectGetMaxX(adjustedVisibleMapRect) / gridSize) * gridSize;
double endY = floor(MKMapRectGetMaxY(adjustedVisibleMapRect) / gridSize) * gridSize;
// for each square in our grid, pick one annotation to show
gridMapRect.origin.y = startY;
while (MKMapRectGetMinY(gridMapRect) <= endY) {
gridMapRect.origin.x = startX;
while (MKMapRectGetMinX(gridMapRect) <= endX) {
NSSet *allAnnotationsInBucket = [self.allAnnotationsMapView annotationsInMapRect:gridMapRect];
NSSet *visibleAnnotationsInBucket = [self.mapView annotationsInMapRect:gridMapRect];
// we only care about PhotoAnnotations
NSMutableSet *filteredAnnotationsInBucket = [[allAnnotationsInBucket objectsPassingTest:^BOOL(id obj, BOOL *stop) {
return ([obj isKindOfClass:[PhotoAnnotation class]]);
}] mutableCopy];
if (filteredAnnotationsInBucket.count > 0) {
PhotoAnnotation *annotationForGrid = (PhotoAnnotation *)[self annotationInGrid:gridMapRect usingAnnotations:filteredAnnotationsInBucket];
[filteredAnnotationsInBucket removeObject:annotationForGrid];
// give the annotationForGrid a reference to all the annotations it will represent
annotationForGrid.containedAnnotations = [filteredAnnotationsInBucket allObjects];
[self.mapView addAnnotation:annotationForGrid];
for (PhotoAnnotation *annotation in filteredAnnotationsInBucket) {
// give all the other annotations a reference to the one which is representing them
annotation.clusterAnnotation = annotationForGrid;
annotation.containedAnnotations = nil;
// remove annotations which we've decided to cluster
if ([visibleAnnotationsInBucket containsObject:annotation]) {
CLLocationCoordinate2D actualCoordinate = annotation.coordinate;
[UIView animateWithDuration:0.3 animations:^{
annotation.coordinate = annotation.clusterAnnotation.coordinate;
} completion:^(BOOL finished) {
annotation.coordinate = actualCoordinate;
[self.mapView removeAnnotation:annotation];
}];
}
}
}
gridMapRect.origin.x += gridSize;
}
gridMapRect.origin.y += gridSize;
}
}
By following above steps we can achieve clustering on mapview, it is not necessary to use any third party code or framework. Please check the Apple sample code here. Please let me know if you have any doubts on this.
Have you looked at ADClusterMapView ? https://github.com/applidium/ADClusterMapView
It does precisely just this.
I just wanted to clustering pins, just showing its number. The following one
https://www.cocoacontrols.com/controls/qtree-objc fits my expectations.
I recently forked off of ADClusterMapView mentioned in another answer and resolved many, if not all, of the issues associated with the project. It's a kd-tree algorithm and animates the clustering.
It's available open source here https://github.com/ashare80/TSClusterMapView
Try this framework (XMapView.framework); it now supports iOS 8.
This framework doesn't need you to change your current project structure and it can directly be used to your MKMapView. There is a zip file. It gives you an example to cluster 200 pins at once. After I tested it in an iPod I found it is very smooth.
http://www.xuliu.info/xMapView.html
This library supports:
clustering different categories
clustering all categories
setting up your own cluster radius and so on
hide or show a certain of categories
individually handle and control each pin in the map
There is a pretty cool and well maintained library for both Objective-C and Swift here: https://github.com/bigfish24/ABFRealmMapView
It does clustering really well and also handles large amounts of points due to its integration with Realm.

MapView - Have Annotations Appear One at a Time

I'm currently adding annotations to my map through a loop... but the annotations are only appearing on my map in groups. Also, on load, only about 4 annotations are actually displayed on the map... but as I move the map a little, all of the annotations that should be there, suddenly appear.
How can I get all of the annotations to load in the right place, one at a time?
Thanks in advance!
Here is the code I'm using to add annotations:
NSString *incident;
for (incident in weekFeed) {
NSString *finalCoordinates = [[NSString alloc] initWithFormat:#"%#", [incident valueForKey:#"coordinates"]];
NSArray *coordinatesArray = [finalCoordinates componentsSeparatedByString:#","];
latcoord = (#"%#", [coordinatesArray objectAtIndex:0]);
longcoord = (#"%#", [coordinatesArray objectAtIndex:1]);
// Final Logs
NSLog(#"Coordinates in NSString: [%#] - [%#]", latcoord, longcoord);
CLLocationCoordinate2D coord;
coord.latitude = [latcoord doubleValue];
coord.longitude = [longcoord doubleValue];
DisplayMap *ann = [[DisplayMap alloc] init];
ann.title = [NSString stringWithFormat: #"%#", [incident valueForKey:#"incident_type"]];
ann.subtitle = [NSString stringWithFormat: #"%#", [incident valueForKey:#"note"]];
ann.coordinate = coord;
[mapView addAnnotation:ann];
[ann release];
}
// Custom Map Markers
-(MKAnnotationView *)mapView:(MKMapView *)map viewForAnnotation:(id <MKAnnotation>)annotation {
if ([annotation isKindOfClass:[MKUserLocation class]])
return nil; //return nil to use default blue dot view
static NSString *AnnotationViewID = #"annotationViewID";
MKAnnotationView *annotationView = (MKAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:AnnotationViewID];
if (annotationView == nil) {
annotationView = [[[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:AnnotationViewID] autorelease];
}
annotationView.canShowCallout = YES;
if ([annotationView.annotation.title isEqualToString:#"one"]) {
UIImage *pinImage = [UIImage imageNamed:#"marker_1.png"];
[annotationView setImage:pinImage];
}
if ([annotationView.annotation.title isEqualToString:#"two"]) {
UIImage *pinImage = [UIImage imageNamed:#"marker_2.png"];
[annotationView setImage:pinImage];
}
annotationView.annotation = annotation;
return annotationView;
}
- (void) mapView:(MKMapView *)mapV didAddAnnotationViews:(NSArray *)views {
CGRect visibleRect = [mapV annotationVisibleRect];
for (MKAnnotationView *view in views) {
CGRect endFrame = view.frame;
CGRect startFrame = endFrame; startFrame.origin.y = visibleRect.origin.y - startFrame.size.height;
view.frame = startFrame;
[UIView beginAnimations:#"drop" context:NULL];
[UIView setAnimationDuration:0.4];
view.frame = endFrame;
[UIView commitAnimations];
}
}
Adam,
This solution is a bit messy as I had to munge up one of my current projects to test, but hopefully this will work for you.
First an explanation, it's critical to separate data from UI presentation. The [MKMapView addAnnotation(s)] are just a data update to MKMapView and have no direct impact on animation or timing.
The delegate method mapView:didAddAnnotationViews: is where all of the custom presentation behavior should be defined. In your description you didn't want these to appear all at once, so you need to sequence your animations instead of performing them simultaneously.
One method is to add all of the annotations at once and then just add them with increasing animation delays, however new annotations that get added for whatever reason will begin their animations at zero again.
The method below sets up an animation queue self.pendingViewsForAnimation (NSMutableArray) to hold annotation views as they are added and then chains the animation sequentially.
I've replaced the frame animation with alpha to focus on the animation problem to separate it from the issue of some items not appearing. More on this after the code...
// Interface
// ...
// Add property or iVar for pendingViewsForAnimation; you must init/dealloc the array
#property (retain) NSMutableArray* pendingViewsForAnimation;
// Implementation
// ...
- (void)processPendingViewsForAnimation
{
static BOOL runningAnimations = NO;
// Nothing to animate, exit
if ([self.pendingViewsForAnimation count]==0) return;
// Already animating, exit
if (runningAnimations)
return;
// We're animating
runningAnimations = YES;
MKAnnotationView* view = [self.pendingViewsForAnimation lastObject];
[UIView animateWithDuration:0.4 animations:^(void) {
view.alpha = 1;
NSLog(#"Show Annotation[%d] %#",[self.pendingViewsForAnimation count],view);
} completion:^(BOOL finished) {
[self.pendingViewsForAnimation removeObject:view];
runningAnimations = NO;
[self processPendingViewsForAnimation];
}];
}
// This just demonstrates the animation logic, I've removed the "frame" animation for now
// to focus our attention on just the animation.
- (void) mapView:(MKMapView *)mapV didAddAnnotationViews:(NSArray *)views {
for (MKAnnotationView *view in views) {
view.alpha = 0;
[self.pendingViewsForAnimation addObject:view];
}
[self processPendingViewsForAnimation];
}
Regarding your second issue, items are not always appearing until you move the map. I don't see any obvious errors in your code, but here are some things I would do to isolate the problem:
Temporarily remove your mapView:didAddAnnotationViews:, mapView:annotationForView: and any other custom behaviors to see if default behavior works.
Verify that you have a valid Annotation at the addAnnotation: call and that the coordinates are visible (use [mapView visibleMapRect], MKMapRectContainsPoint(), and MKMapPointForCoordinate().
If it is still not functioning, look at where you are calling the add annotations code from. I try to avoid making annotation calls during map movement by using performSelector:withObject:afterDelay. You can precede this with an [NSObject cancelPreviousPerformRequestsWithTarget:selector:object:] to create a slight delay prior to loading annotations in case the map is being moved a long distance with multiple swipes.
One last point, to achieve the pin-drop effect you're looking for, you probably want to offset by a fixed distance from the original object instead of depending on annotationVisibleRect. Your current implementation will result in pins moving at different speeds depending on their distance from the edge. Items at the top will slowly move into place while items at the bottom will fly rapidly into place. Apple's default animation always drops from the same height. An example is here: How can I create a custom "pin-drop" animation using MKAnnotationView?
Hope this helps.
Update:
To demonstrate this code in action I've attached a link to a modified version of Apple's Seismic demo with the following changes:
Changed Earthquake.h/m to be an MKAnnotation object
Added SeismicMapViewController.h/m with above code
Updated RootViewController.h/m to open the map view as a modal page
See: http://dl.dropbox.com/u/36171337/SeismicXMLWithMapDelay.zip
You will need to pause updating after you add each annotation and allow the map to have time to refresh.

Reverse Geocoding on MKMapView by dragging Pins

I have a subclass "Location" of NSManagedObject and this class conforms to MKAnnotation.
When I add a Location to a Map...the ReverseGeocoder starts. By clicking the pin I see in the callout View the address of the pin. So everything works fine.
Now my problem.
I am able to drag the pins. When I am finished dragging, than again the ReverseGeocoder starts to ask for location Data. But I still have the old address in the callout view, although the placemarks are updated in the Location object. After dragging again I can see the new address. (of course the new one isn't new anymore, because I dragged).
Here is my code for the dragging state:
- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)annotationView didChangeDragState:(MKAnnotationViewDragState)newState fromOldState:(MKAnnotationViewDragState)oldState{
//if dragging ended
if (newState == MKAnnotationViewDragStateNone && oldState == MKAnnotationViewDragStateEnding) {
CLLocation *location = [[CLLocation alloc] initWithLatitude:annotationView.annotation.coordinate.latitude longitude:annotationView.annotation.coordinate.longitude];
[self geocodeLocation:location forAnnotation:annotationView.annotation];
[location release];
}
And the Code for my reverse geocoder delegate
- (void)reverseGeocoder:(MKReverseGeocoder*)geocoder didFindPlacemark:(MKPlacemark*)place
{
Location* theAnnotation = [self.map annotationForCoordinate:place.coordinate];
if (!theAnnotation)
return;
// Associate the placemark with the annotation.
theAnnotation.city = [place locality];
theAnnotation.zipcode = [place postalCode];
theAnnotation.street = [place thoroughfare];
DEBUGLOG(#"Place: %#", place);
// Add a More Info button to the annotation's view.
MKPinAnnotationView* view = (MKPinAnnotationView*)[self.map viewForAnnotation:theAnnotation];
if (view)
{
DEBUGLOG(#"SUBTITLE: %#", theAnnotation.subtitle);
view.leftCalloutAccessoryView = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
}
}
Any help would be wonderful. In the end the Maps App is best example for doing that by dragging.
I figured out.
Because of KVO I hust hace to set the subtitle of the Location object. So it gets displayed.
But because the subtilte is generated by
// Associate the placemark with the annotation.
theAnnotation.city = [place locality];
theAnnotation.zipcode = [place postalCode];
theAnnotation.street = [place thoroughfare];
I just had to do the following in my NSManagedObject Subclass.
- (void) setSubtitle:(NSString *)title{ //do nothing}

iPhone MapKit - update annotations coordinates and map

I use this tutorial for integrating MapKit to my application:
http://iphonebcit.wordpress.com/iphone-map-kit-tutorial/
CLLocationCoordinate2D coordinate;
coordinate.latitude = 49.2802;
coordinate.longitude = -123.1182;
NSUInteger count = 1;
for(int i = 0; i < 10; i++) {
CGFloat latDelta = rand()*.035/RAND_MAX - .02;
CGFloat longDelta = rand()*.03/RAND_MAX - .015;
CLLocationCoordinate2D newCoord = {coordinate.latitude+latDelta, coordinate.longitude+longDelta};
MapDemoAnnotation* annotation = [[MapDemoAnnotation alloc] initWithCoordinate:newCoord andID:count++];
[mapView addAnnotation:annotation];
[annotation release];
}
and
- (MKAnnotationView *)mapView:(MKMapView *)mapViewLocal viewForAnnotation:(id <MKAnnotation>)annotation {
MKPinAnnotationView *pinView = (MKPinAnnotationView*)[mapViewLocal dequeueReusableAnnotationViewWithIdentifier:#"Pin"];
if(pinView == nil) {
pinView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:#"Pin"];
pinView.pinColor = MKPinAnnotationColorPurple;
pinView.rightCalloutAccessoryView = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
pinView.animatesDrop = YES;
pinView.canShowCallout = YES;
} else {
pinView.annotation = annotation;
}
return pinView;
}
So the pins will set on the map randomly. In my application, the coordinates will change. How can I change the coordinates of the annotations, so they will be updated on the map?
Does anyone know?
At iPhone SDK 3.x you have to remove the pin annotations and set it again. That is not very nice if you have many annotations an your map.
I try to make it better so I ony display/renew my pin annotations which are on the screen. So if an user zoom in to New York, there won't be pin annotations in San Francisco or other than the user can't see. So the performance will be much better.
Perhaps in the future this would be possible. I hope so :-)
That tutorial is only for getting a map view to show, nothing else. You're going to need a bigger tutorial. I found this one useful:
http://blog.objectgraph.com/index.php/2009/04/02/iphone-sdk-30-playing-with-map-kit/
Probably what you'll need to do is to loop through all the annotations on the map, removing them, then loop through your data array, adding the annotations back again. You could be more clever about it and loop through the data array, checking if there's already a pin with the same latitude/longitude on the map, but that way gets more complicated.
Since I'm doing annotations at the moment I just made a quick test. You do get a compiler warning, so it might not be supported. But it works.
Make a custom MKAnnotation class so you can set the coordinate property to be writable:
#property (nonatomic, readwrite) CLLocationCoordinate2D coordinate;
Then at whatever event or interval you want, change the coordinate using something like this:
CLLocation *loc=[[CLLocation alloc] initWithLatitude:55.0 longitude:17.0];
annotation.coordinate=loc.coordinate;
[loc release];