`
janedoneway
  • 浏览: 582197 次
  • 性别: Icon_minigender_1
  • 来自: 广州
社区版块
存档分类
最新评论

MapKit annotation drag and drop with callout info update

 
阅读更多

MapKit annotation drag and drop with callout info update

 

 

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


Introduce MapKitDragAndDrop
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];
}
view raw MapKit1.m This Gist brought to you by GitHub.

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 {
}
view raw MapKit2.m This Gist brought to you by GitHub.

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;
}
view raw MapKit3.m This Gist brought to you by GitHub.

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:

markkrenek said...

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.

hollowout said...

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.

Dzamir said...

Great article! Thanks for the code sharing!

Carl said...

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

brian said...

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

hollowout said...

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

brian said...

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!

ryoji Ishii said...

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.

20041102.Last.Chance said...

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?

20041102.Last.Chance said...

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

hollowout said...

This because user's finger touchBegin usually reach parts of the pin (DDannotationView).

Harry said...

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

John Fowler said...

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

digdog said...

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

Links to this post

 

分享到:
评论

相关推荐

    (0158)-iOS/iPhone/iPAD/iPod源代码-地图(Map)-MapKit Drag And Drop

    实现地图标注(Annotation)的拖放、移动效果。用户可以用手指按住地图上的annotation,从而对其进行拖放、移动位置。当用户拖放annotation时,annotation旁边还出现投影效果。 注意:请在Mac下解压使用

    MapKit Drag And Drop(iPhone源代码)

    来源:Licence:MIT平台:iOS设备:iPhone / iPad作者:Ching-Lan HUANG  实现地图标注(Annotation...当用户拖放annotation时,annotation旁边还出现投影效果。 Code4App编译测试,适用环境:Xcode 4.3, iOS 5.0。

    IOS开发之——MapKit地图

    本文将深入探讨MapKit地图的相关知识点,结合提供的资源——"grgcombs-MultiRowCalloutAnnotationView-76927b9",我们将探讨自定义Callout Annotation View和多行注解视图的实现。 首先,MapKit的基础使用包括设置...

    MapKit之在地图上加pin(转)

    下面我们将深入讲解MapKit的核心概念、如何创建自定义Annotation以及在地图上添加Pin的步骤。 首先,了解MapKit的基本概念。MapKit框架提供了MKMapView类,它是显示地图的主要视图。通过MKMapView,我们可以加载...

    custom_callout_part1_src

    在这个名为"custom_callout_part1_src"的项目中,我们将探讨如何实现iPhone地图上点击Annotation后展示自定义Callout的第一部分源代码。 1. **自定义Annotation**: 在MapKit中,我们首先需要创建一个自定义...

    controllers-swift:discontrol导航控制器(segue和porcòdigo),MapKit,Annotation和Callout Personalizados

    在iOS应用开发中,`controllers-swift`是一个项目,它深入展示了Swift编程语言在构建用户界面,特别是导航控制器、UIStoryboardSegue、自定义MapKit标注和Callout等方面的应用。在这个项目中,开发者可以学习如何...

    自定义的Annotation

    在iOS开发中,Annotation是苹果地图(MapKit)框架中的一个重要概念,用于在地图上添加可视化标记,以展示特定地理位置的信息。自定义的Annotation则允许开发者根据需求个性化地图上的标注,比如添加图片、自定义...

    IOS MapKit应用

    MapKit是iOS SDK中的一个核心框架,用于在iOS应用程序中集成地图功能。它与Apple的Map服务紧密集成,提供了一套完整的API,开发者可以利用这些API实现地图显示、定位、路线规划等多种功能。本篇文章将深入探讨MapKit...

    ios-mapKit.zip

    MapKit是iOS操作系统中的一个核心框架,用于在应用程序中集成地图功能。这个框架与Apple的Map服务紧密相连,允许开发者创建交互式地图、定位用户、显示兴趣点(POIs)、规划路线等。在iOS应用开发中,MapKit是实现...

    Mapkit 集群功能实例

    mapkit 上有时候会有很多annotation,如果全部照的原样显示出来未免有些杂乱,而且有的会重合。实例里根据zoom级别来自动调整显示的annotation,如果有重合的,用一个带有annotation计数的圆点图片来代替,这样简洁...

    MKMapKit系统地图相关demo

    MKMapKit是Apple提供的一套强大的地图框架,用于在iOS和macOS应用中集成地图功能。这个框架允许开发者创建各种地图界面,包括显示地图、添加标注、实现路线规划以及定位等。下面我们将深入探讨MKMapKit提供的核心...

    mapkit-custom-callout:有关如何使用MapKit创建自定义标注的示例

    MapKit的自定义标注。描述目标: 能够轻松插入自己的自定义视图作为标注显示标注后,您仍然应该可以与地图互动能够创建多种类型的图钉能够将事件附加到地图标注当标注显示在地图的边缘时,应将地图移动得恰到好处,...

    地图定位mapkit

    mapkit地图定位,自己编辑当前的位置

    用CLLocation与mapkit实现定位与搜索方位功能 iOS

    确保在`Info.plist`文件中添加相应的使用条款描述。 **三、启动定位服务** 在获得授权后,可以启动定位服务: ```swift locationManager.startUpdatingLocation() ``` 当位置改变时,`CLLocationManagerDelegate`的...

    MapKit 地图使用

    MapKit是苹果iOS和macOS平台上的一个强大框架,它为开发者提供了集成地图服务的能力,让用户可以在自己的应用程序中轻松地展示、操作和交互地图。在本文中,我们将深入探讨MapKit的基本概念、核心功能以及如何在实践...

    More.iPhone.Development.with.Swift.Exploring.the.iOS.SDK

    The update to the bestselling More iPhone Development by Dave Mark and Jeff LaMarche, More iPhone Development with Swift digs deeper into the new Apple Swift programming language and iOS 8 SDK, ...

    IOS应用源码之Apple的MapKit相关官方Sample三个送上WeatherMap.zip

    MapKit支持自定义的Annotation视图,用于在地图上显示特定的图标或信息。例如,"WeatherMap"可能展示了如何创建自定义天气图标作为Annotation,以表示不同地点的天气状况。这通常涉及到MKAnnotation协议的实现,...

    More iPhone Development with Swift(Apress,2015)

    The update to the bestselling More iPhone Development by Dave Mark and Jeff LaMarche, More iPhone Development with Swift digs deeper into the new Apple Swift programming language and iOS 8 SDK, ...

    (0010)-iOS/iPhone/iPAD/iPod源代码-地图(Map)-Map Callouts

    3. 自定义Callout视图:默认的注解气泡可能不足以满足需求,你可以通过实现`MKMapViewDelegate`的`viewFor annotation:`方法来自定义callout视图。 ```swift func mapView(_ mapView: MKMapView, viewFor ...

Global site tag (gtag.js) - Google Analytics