The
MapKitDragAndDrop project now goes to
Version 3 with iOS 4 MapKit built-in draggable support, and the whole project had shifted to
modern runtime, using
Objective-C 2.0 ABI and
LLVM 1.5 compiler. So you will have
smaller and
faster binary that runs on
both iOS 4 and ealier iPhone OS 3.1.x/3.2 at the same time.
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.
8/01/2009 8:29 AMIn 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!
8/01/2009 9:12 AMI 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!
8/23/2009 7:41 AMYou 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)
9/03/2009 4:57 AMThank 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?
9/06/2009 6:59 AMThanks,
Brian
Brian,
9/06/2009 8:13 AMI 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!
9/06/2009 11:41 AMHI, finally found the code I was looking for.
10/07/2009 7:11 AMI'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?
10/08/2009 9:03 AMOh... 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!)
10/08/2009 9:13 AMThis because user's finger touchBegin usually reach parts of the pin (DDannotationView).
10/08/2009 10:19 AMThis is really nice tutorial ,and blog too. Thanks a ton.
10/09/2009 6:57 AMI 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!
10/23/2009 6:29 PMI'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.
11/22/2009 12:09 AMYou can leave comments over digdog.tumblr.com.
- digdog
Post a Comment
Links to this post