Old and Deprecated
There're few samples [1][2] about creating a MapKit app for iPhone, and Apple only provides snippets of code for people who want to dig out the secret of MapKit. So I decide to create my own MapKit sample that meets my needs. The sample code is available on my GitHub, MapKitDragAndDrop, feel free to download it.
In this sample, you can:
- Use CLLocationManager to find out current location
- Use MKReverseGeocoder to convert current location coordinate to place information
- Customize annotation/pin callout info
- Update callout info when MKPlacemark is found by MKReverseGeocoder
- And finally, allow annotation/pin to be able to drag and drop
UPDATE 2
Thanks to Uffe Overgaard Koch's help, the MapKitDragAndDrop now can update callout info automatically in both 3.0 and 3.1 SDK.
UPDATE
In 3.0 SDK, the callout view info won't update unless you tap to close it and tap pin again to bring it back. But in 3.1 SDK, the bug seems fixed, the MapKit will update the callout view info automatically when DDAnnotation changed.
UIViewController
The sample works pretty straight forward, it all begins by creating CLLocationManager to update location in UIViewController's viewDidLoad:
- (void)viewDidLoad {
[super viewDidLoad];
_mapView.showsUserLocation = YES;
// Start by locating current position
self.locationManager = [[CLLocationManager alloc] init];
_locationManager.delegate = self;
[_locationManager startUpdatingLocation]; }
Once CLLocationManager updates the location, we create DDAnnotation and add into the map view. Then we stop CLLocationManager from updating location again, otherwise you will see another new annotation drop on the map.
#pragma mark -#pragma mark CLLocationManagerDelegate methods
- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation {
// Add annotation to map
DDAnnotation *annotation = [[DDAnnotation alloc] initWithCoordinate:newLocation.coordinate title:@"Drag to move Pin"];
[_mapView addAnnotation:annotation];
[annotation release];
// We only update location once, and let users to do the rest of the changes by dragging annotation to place they want
[manager stopUpdatingLocation];
}
- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error {
}
When we add newly created annotation to the map view, the MKMapViewDelegate methods take over. We prepare our own annotation view in mapView:viewForAnnotation:.
First, we avoid user's current location annotation (MKUserLocation, the blue round spot you saw on the map) use our custom annotation view. And then, we try to dequeue our annotation view from map view, if there's no existing DDAnnotationView for us to reuse, we create a new one.
Finally, the tricky part, we assign the map view to DDAnnotationView before return. This is because when later user dragging the annotation/pin to new position, we will need to use map view's convertPoint:toCoordinateFromView: to get the new coordinate.
#pragma mark -#pragma mark MKMapViewDelegate methods
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation {
if (annotation == mapView.userLocation) {
return nil;
}
DDAnnotationView *annotationView = (DDAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:@"Pin"];
if (annotationView == nil) {
annotationView = [[[DDAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:@"Pin"] autorelease];
}
// Dragging annotation will need _mapView to convert new point to coordinate;
annotationView.mapView = mapView;
return annotationView;
}
MKAnnotationView and MKAnnotation
We create our own MKAnnotationView and MKAnnotation, and named them DDAnnotationView and DDAnnotation.
The DDAnnotation is designed to work like MKPlacemark or MKUserLocation, is a specific type of annotation that provides custom title and auto coordinate-to-placemark subtitle for DDAnnotationView, MKPinAnnotationView or other MKAnnotationView to use later.
@interface DDAnnotation : NSObject <MKAnnotation, MKReverseGeocoderDelegate> {
@private CLLocationCoordinate2D _coordinate;
NSString *_title;
MKPlacemark *_placemark;
}
- (id)initWithCoordinate:(CLLocationCoordinate2D)coordinate title:(NSString*)title;
- (void)changeCoordinate:(CLLocationCoordinate2D)coordinate;
@end
And this is how DDAnnotation works: When you create DDAnnotation with initWithCoordinate:title:, we save the coordinate and title, and create MKReverseGeocoder to try to reverse coordinate.
Once MKReverseGeocoder converts the geocode to placemark information, MKReverseGeocoderDelegate mehod reverseGeocoder:didFindPlacemark: gets called, and we save the MKPlacemark, and post MKAnnotationCalloutInfoDidChangeNotification notification, to let the map view know about the change. Then, map view will ask DDAnnotationView to retrieve DDAnnotation's title: and subtitle: and update the callout info.
#import "DDAnnotation.h"
@interface DDAnnotation ()
@property (nonatomic, retain) NSString *title;
@property (nonatomic, retain) MKPlacemark *placemark;
- (void)notifyCalloutInfo:(MKPlacemark *)placemark;
@end
#pragma mark -#pragma mark DDAnnotation implementation
@implementation DDAnnotation
@synthesize coordinate = _coordinate; // property declared in MKAnnotation.h
@synthesize title = _title;
@synthesize placemark = _placemark;
- (id)initWithCoordinate:(CLLocationCoordinate2D)coordinate title:(NSString*)title {
if ((self = [super init])) {
[self changeCoordinate:coordinate];
_title = [title retain];
_placemark = nil;
}
return self;
}
#pragma mark -#pragma mark MKAnnotation Methods
- (NSString *)title {
return _title;
}
- (NSString *)subtitle {
if (_placemark) {
return [[_placemark.addressDictionary valueForKey:@"FormattedAddressLines"] componentsJoinedByString:@", "];
}
return [NSString stringWithFormat:@"%lf, %lf", _coordinate.latitude, _coordinate.longitude];
}
#pragma mark -#pragma mark Change coordinate
- (void)changeCoordinate:(CLLocationCoordinate2D)coordinate {
_coordinate = coordinate;
// Try to reverse geocode here
// Note: LLVM/Clang Static analyzer might report potentical leak, but it won't because we release in delegate methods
MKReverseGeocoder *reverseGeocoder = [[MKReverseGeocoder alloc] initWithCoordinate:_coordinate];
reverseGeocoder.delegate = self;
[reverseGeocoder start];
}
#pragma mark -#pragma mark MKReverseGeocoderDelegate methods
- (void)reverseGeocoder:(MKReverseGeocoder *)geocoder didFindPlacemark:(MKPlacemark *)newPlacemark {
[self notifyCalloutInfo:newPlacemark];
geocoder.delegate = nil;
[geocoder release];
}
- (void)reverseGeocoder:(MKReverseGeocoder *)geocoder didFailWithError:(NSError *)error {
[self notifyCalloutInfo:nil];
geocoder.delegate = nil;
[geocoder release];
}
#pragma mark -#pragma mark MKAnnotationView Notification
- (void)notifyCalloutInfo:(MKPlacemark *)newPlacemark {
[self willChangeValueForKey:@"subtitle"]; // Workaround for SDK 3.0, otherwise callout info won't update.
self.placemark = newPlacemark;
[self didChangeValueForKey:@"subtitle"]; // Workaround for SDK 3.0, otherwise callout info won't update.
[[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:@"MKAnnotationCalloutInfoDidChangeNotification" object:self]];
}
#pragma mark -#pragma mark Memory Management
- (void)dealloc {
[_title release], _title = nil;
[_placemark release], _placemark = nil;
[super dealloc];
}
@end
As for DDAnnotationView, this is the part we handle drag and drop events and setup callout view. The event handling code was copied from Apple's iPhone Application Programming Guide, you can check more detail from here.
But, basically, we implement all four touch event methods, which are touchesBegan: withEvent: for recording information about the initial touch event, let us know the start location of the movement, and touchesMoved:withEvent: method adjusts the position of the view by checking the new position to see if the dragging is actually happened.
When the user stops dragging an annotation view, the touchesEnded:withEvent: method takes over, we use map view's convertPoint:toCoordinateFromView: to convert new pixel point back to map coordinate value, and set the new value back to this DDAnnotationView's annotation object with DDAnnotation's changeCoordinate: method. We are doing this was because MKAnnotation's coordinate property is readonly, you are not allow to modify it by default, so we create a "setter" method to do the change.
And Lastly, the touchesCancelled:withEvent: method, this is not a optional method, if you decide to take care touch event, do not ignore this one (Apple said so). We reset the position and states here if draggin is not detected in ouchesMoved:withEvent:.
#import "DDAnnotationView.h"#import "DDAnnotation.h"
#pragma mark -#pragma mark DDAnnotationView implementation
@implementation DDAnnotationView
@synthesize mapView = _mapView;
- (id)initWithAnnotation:(id <MKAnnotation>)annotation reuseIdentifier:(NSString *)reuseIdentifier {
if ((self = [super initWithAnnotation:annotation reuseIdentifier:reuseIdentifier])) {
self.enabled = YES;
self.canShowCallout = YES;
self.multipleTouchEnabled = NO;
self.animatesDrop = YES; }
return self;
}
#pragma mark -#pragma mark Handling events
// Reference: iPhone Application Programming Guide > Device Support > Displaying Maps and Annotations > Displaying Annotations > Handling Events in an Annotation View
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// The view is configured for single touches only.
UITouch* aTouch = [touches anyObject];
_startLocation = [aTouch locationInView:[self superview]];
_originalCenter = self.center;
[super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch* aTouch = [touches anyObject];
CGPoint newLocation = [aTouch locationInView:[self superview]];
CGPoint newCenter;
// If the user's finger moved more than 5 pixels, begin the drag.
if ((abs(newLocation.x - _startLocation.x) > 5.0) || (abs(newLocation.y - _startLocation.y) > 5.0)) {
_isMoving = YES; }
// If dragging has begun, adjust the position of the view.
if (_mapView && _isMoving) {
newCenter.x = _originalCenter.x + (newLocation.x - _startLocation.x);
newCenter.y = _originalCenter.y + (newLocation.y - _startLocation.y);
self.center = newCenter;
} else {
// Let the parent class handle it.
[super touchesMoved:touches withEvent:event]; }
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if (_mapView && _isMoving) { // Update the map coordinate to reflect the new position.
CGPoint newCenter = self.center;
DDAnnotation* theAnnotation = (DDAnnotation *)self.annotation;
CLLocationCoordinate2D newCoordinate = [_mapView convertPoint:newCenter toCoordinateFromView:self.superview];
[theAnnotation changeCoordinate:newCoordinate];
// Clean up the state information.
_startLocation = CGPointZero;
_originalCenter = CGPointZero;
_isMoving = NO; } else {
[super touchesEnded:touches withEvent:event]; }
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
if (_mapView && _isMoving) {
// Move the view back to its starting point.
self.center = _originalCenter;
// Clean up the state information.
_startLocation = CGPointZero;
_originalCenter = CGPointZero;
_isMoving = NO;
} else {
[super touchesCancelled:touches withEvent:event]; }
}
@end
That's it. Hope you enjoy the ride on the map.
References:
[1] Craig Spitzkoff's Using MKAnnotation, MKPinAnnotationView and creating a custom MKAnnotationView in an MKMapView
[2] Gavi Narra's Playing with Map Kit series: Part 1, Part 2 and Part 3.
14 comments:
There's a crash bug in the code due to accessing a released object.
In reverseGeocoder:geocoder didFindPlacemark:, _placemark needs to be retained.
I actually made _placemark a property so that assignment could be used like this:
self._placemark = placemark;
self._placemark = nil;
_placemark also needs to be released in dealloc.
Thank you for the review, man! Good Job!
I fix the problem you discovered, and commited to the github. I create a private property for _placemark ivar, so we can use property self.placemark within the class and need not to expose to others.
Great article! Thanks for the code sharing!
You saved my butt with the reference to the observation of the subtitle property. I thought I was screwed until I found your article (1 of 11 hits on google about MKAnnotationCalloutInfoDidChangeNotification)
Thank you,
Carl Coryell-Martin
Hi, this code helped me a lot, thank you. I'm having an issue, however. Have you noticed multiple pins dropping when you run your program? When I run yours, sometimes I get only one pin, but sometimes locationUpdate is called twice and I get two pins falling at exactly the same point. Do you know what the issue could be?
Thanks,
Brian
Brian,
I tried, but I didn't see second pin drop in the sample.
When Location Manager updates its location, the delegate method locationManager:didUpdateToLocation:fromLocation: gets called, and we create the pin ad drop it on the map. However, in the sample, we ask manager to stopUpdatingLocation after that, so it should no longer update again.
If you saw two pin at the same point, the most possible situation is: after stopUpdatingLocation called, and before location manager actually STOPED updating, the locationManager:didUpdateToLocation:fromLocation: update again. I didn't see this situation on my mac/iPod, but this can be fixed easily by add a flag in the delegate method:
if (self.boolDidUpdateLocation == NO) {
// Add annotation here
// Stop updating
[manager stopUpdatingLocation];
self.boolDidUpdateLocation = YES;
}
Yeah, sometimes I would get only 1 pin, but sometimes I would get 2 or even 3. But setting a BOOL flag like you wrote is the option I went for. Thanks again!
HI, finally found the code I was looking for.
I'm having a question to ask,
by clicking button is it possible to link to other viewer?
like
- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control
{
if ([control isKindOfClass:[UIButton class]]) {
//DetailViewController *dtv = [[DetailViewController alloc] initWithNibName:@"DetailView" bundle:[NSBundle mainBundle]];
//[self.navigationController pushViewController:dtv animated:NO];
//[dtv release];
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"http://hollowout.blogspot.com"]];
}
}
I tried to write this code but it didnt work, Can you Help me please.
I'm finding this app will only allow me to drag the pin once on my 3.1 simulator, but I can re-drag it many times on my iPhone running 3.1. Any idea why that might be?
Oh... I can drag it more than once in simulator. Might be because I added the boolDidUpdateLocation code suggested above... either way its hard to grab and drag the sucker. Don't know if its me being incapable of clicking on the exact right spot, or if something intermittent is happening. (Thanks for posting this sample code by the way!)
This because user's finger touchBegin usually reach parts of the pin (DDannotationView).
This is really nice tutorial ,and blog too. Thanks a ton.
I was trying to follow ur blog, but fortunately there is no option available :( ... u r doing great stuff, and i think, soon u wud allow others to follow ur blogs.
Thanks again :)
Great sample. Thanks!
I'm running into weird behavior with pinch zoom now. I can zoom out; but not in. Actually, when trying to zoom in, it zooms out.
Anyone else run across this behavior?
Thanks in advance, John
This article had moved to http://digdog.tumblr.com/post/252784277/mapkit-annotation-drag-and-drop-with-callout-info.
You can leave comments over digdog.tumblr.com.
- digdog
Post a Comment
Links to this post