在MKMapView上添加标注可以方便用户更好地获取信息,与地图进行交互。标注分为两种,一种是Annotations
,一种是Overlays
。
Annotations。标注由经纬度所确定的一个点,比如用户当前位置,一个被指定的地址,或者一个被收藏的地点。
Overlays。标注由多点连成的线,一个或者多个相邻或不相邻的区域。比如路线、交通状况、或者某个地点的边界。
和MKMapView中的subView不同,Annotations和Overlays会随着地图的移动而移动。
添加Annotations
Annotations可以是地图上一个点醒目地标注出来,并且可以提供这个地点一些简单的信息。你可以用Annotations来标注当前位置、指定的位置、或者被收藏的位置等等。可以在地图上用一组图片来分别标记这些位置,还可以通过calloutView显示基本信息和可操作的空间,比如链接到更详细的介绍页面。
下面这张图,使用了大头针
来标注一个指定的地点,并且通过calloutView
显示一些基本信息,以及一个点击后可以提供驾车导航的按钮
和点击后可以跳转获取更多信息的按钮
。
如果要定义一个Annotation,要通过下面两个类:
Annotation object ,遵循
MKAnnotation
协议,管理Annotation相关的属性。Annotation View ,
MKAnnotationView
类型,来绘制Annotation的样式。
MapKit已经提供一些标准样式的Annotations,比如上图所示的大头针。也可以自定义annotationView。无论是使用标准的还是自定义的annotationView,你都不能使用addSubView:
的方法将他们添加到mapView上。而应该使用mapView的代理方法mapView:viewForAnnotation:
添加Annotations的步骤
按照以下步骤来实现和使用Annotations。假定已经添加了mapView。
-
用下面任意一种方法定义_Annotation object_。
用
MKPointAnnotation
类实现一个简单的Annotation。用这个方法定义的Annotation object包含calloutView的title和subtitle属性。自定义一个遵循
MKAnnotation
协议的类。这个类可以包含任何你想包含的属性。
-
定义一个_Annotation View_。根据你的需要选择合适的方法。
如果使用系统提供的大头针作为标注,只需要创建一个
MKPinAnnotationView
的实例即可。如果使用一张静态图片,创建一个
MKAnnotationView
的实例,给它的image
属性赋值即可。如果上面两种方法已经无法满足你,那么就新建一个继承自
MKAnnotation
类的子类,实现绘制代码。
-
实现mapView的代理方法
mapView:viewForAnnotation:
。在实现这个方法的时候,如果存在可以复用的annotationView,就直接使用。如果不存在,新建一个annotationView。如果需要显示多种类型的annotationView,根据
annotaion
类型不同,显示相应类型的annotationView。这个方法让我想起
tableView:cellForRowAtIndexPath:
。它们两个的实现方式很相似。 使用
addAnnotation
或者addAnnotations:
方法,添加annotationView到mapView上。
无论被标注的位置是否在可见区域内,annotationView都会被添加到mapView上。如果希望选择性地隐藏annotationView,你必须手动移除它们。
无论mapView的缩放比例是多少,AnnotationView都会以相同的大小显示。因此,当用户将地图比例缩小时,很可能会是AnnotationView会相互遮挡。为了解决这样的问题,可以根据缩放比例,添加或者移除annotationView。比如在一个天气应用中,当缩放比例小的时候,只显示省会城市的天气;当缩放比例变大的时候,可以逐渐显示出地级市、区县、乡镇的天气信息。
MKAnnotation、MKAnnotationView、CalloutView
首先,让我们来理解一下这两个类的作用以及它们的关系。
MKAnnotation类中定义了MKAnnotation
协议,这个协议定义了coordinate(必须实现的属性)、title和subtitle。coordinate属性的作用就是定义在哪个点处显示MKAnnotationView。
所以,一个MKAnnotationView都会对应一个MKAnnotation对象,即它的annotation
属性。而MKAnnotation对象可以适用于多个MKAnnotationView。
MKAnnotationView是用来定义标注的样式。
CalloutView是当MKAnnotationView被选中后,弹出的View,用于呈现更多关于当前标注的位置的信息。默认情况下,它的title和subtitle由MKAnnotationView对象的annotation
属性定义。
接下来,分别介绍它们的用法。
定义Annotation对象
如果仅仅需要关联一个位置的title,你只要用MKPointAnnotation
类作为Annotation对象就行了。如果想添加另外的信息,你需要自定义一个Annotation对象。所有的Annotation对象必须遵循MKAnnotation
协议。
一个自定义的Annotation对象必须包含coordinate
和其他你想要的属性。给出最简单的Annotation对象的定义。
@interface myCustomAnnotation : NSObject {
CLLocationCoordinate2D coordinate;
}
@property (readonly, nonatomic) CLLocationCoordinate2D coordinate;
- (instancetype)initWithLocation:(CLLocationCoordinate2D)coord;
// 其他方法或者属性
@end
自定义的类必须实现coordinate
属性和一个给它赋值的初始化方法。(建议使用@synthesize,可以保证mapkit可以根据这个属性值的改变自动更新地图。)
@implementation myCustomAnnotation
@synthesize coordinate;
- (instancetype)initWithLocation:(CLLocationCoordinate2D)coord {
self = [super init];
if (self != nil) {
coordinate = coord;
}
return self;
}
@end
如果AnnotationView添加到mapView上之后,你手动地修改类中coordinate、title、subtitle属性的值,请务必发送一个通知。MapKit使用KVO检测这三个属性值的变化以在需要的时候更新地图。如果不发送,可能会导致位置的标注没有被正确显示。
使用系统提供的AnnotationView
使用系统提供的annotationView可以很轻松地标注地图。MKAnnotationView
定义了所有annotationView的基本行为。它的子类MKPinAnnotationView
用一张大头针的图片来标注一个位置。
也可以不通过继承,直接设置它的image
属性,来显示一张图片作为annotationView。这张图片是以被标注的位置为中心呈现的。如果不想显示在中心点,你可以使用centerOffset
属性移动中心点。
举个栗子。创建一个自定义图片的MKAnnotationView,并且图片显示在经纬度的右下方。
MKAnnotationView* aView = [[MKAnnotationView alloc] initWithAnnotation:annotation
reuseIdentifier:@"MyCustomAnnotation"];
aView.image = [UIImage imageNamed:@"myimage.png"];
aView.centerOffset = CGPointMake(10, -20);
可以在代理方法mapView:viewForAnnotation:
中创建标准的AnnotationView。
自定义AnnotationView
如果静态图片不能满足你的需求,你就可以通过继承MKAnnotationView
来自定义annotationView。
重写
drawRect:
方法,重新定义样式。
当重写drawRect:
方法时,务必保证annotationView的frame非零,以确保在地图上是可见的。因为默认的初始化方法会用image
属性的图片的frame作为annotationView的frame。
给一个简单的例子,重写了- (instancetype)initWithAnnotation:(id
方法。
- (instancetype)initWithAnnotation:(id)annotation reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithAnnotation:annotation reuseIdentifier:reuseIdentifier];
if (self) {
CGRect myFrame = self.frame;
myFrame.size.width = 40;
myFrame.size.height = 40;
self.frame = myFrame;
self.backgroundColor = [UIColor blueColor];
self.opaque = NO;
}
return self;
}
在代理方法中创建annotationView
当需要添加annotationView时,调用代理方法mapView:viewForAnnotation:
。如果没有实现
这个方法或者总返回nil
的话,系统就会使用默认的annotationView。如果不想使用系统默认的,那就重写这个方法,然后返回MKAnnotationView
对象。
在每次创建新的annotationView时,总要检查是否存在可复用的View。mapView的dequeueReusableAnnotationViewWithIdentifier:
方法可以获取到可以复用的View。如果返回了nil
,那么创建一个新的annotationView。如果没有返回nil
,那么将它的属性值换掉,然后赋给annotationView。__无论是哪种情况,都要把方法中annotation
参数赋给annotationView.annotation
。__
- (MKAnnotationView *)mapView:(MKMapView *)mapView
viewForAnnotation:(id )annotation
{
// 如果标注的是用户当前位置,则直接返回nil。
if ([annotation isKindOfClass:[MKUserLocation class]])
return nil;
// 处理自定义的annotation。
if ([annotation isKindOfClass:[MyCustomAnnotation class]])
{
// 首先尝试复用已存在的MKPinAnnotationView。
MKPinAnnotationView *pinView = (MKPinAnnotationView*)[mapView dequeueReusableAnnotationViewWithIdentifier:@"CustomPinAnnotationView"];
if (!pinView)
{
// 没有可以复用的View,新建一个。
pinView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:@"CustomPinAnnotationView"];
pinView.pinColor = MKPinAnnotationColorRed;
pinView.animatesDrop = YES;
pinView.canShowCallout = YES;
// 如果有的话,可以通过设置accessoryView定义callout。
}
else
pinView.annotation = annotation;
return pinView;
}
return nil;
}
创建Callout
Callout是在annotationView被选中时弹出。这时,AnnotationView的selected
属性为YES。你可以通过setSelected:
方法设置selected属性,手动控制CalloutView的显示和消失。
同样地,它既可以是系统提供的标准View,也可以是自定义View。一个标准的callout会显示标注的title,此外,它还可以显示subtitle、image和一个UIControl对象。如果想自定义callout,给annotationView添加自定义的subView,然后重写hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
方法响应用户事件。
使用标准的callout是显示自定义内容最容易的方法。如下图所示,在callout中添加了图片和详情按钮。
接下来给出代码,如何实现修改callout样式。
// 假设这个annotationView已经添加到mapView上了。
- (MKAnnotationView *)mapView:(MKMapView *)theMapView viewForAnnotation:(id )annotation
{
// 首先尝试重用pin view。(代码没有贴出来,请参照上一段代码)。
// 如果没有可以重用的View,则新建一个对象。
MKPinAnnotationView *customPinView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:BridgeAnnotationIdentifier];
customPinView.pinColor = MKPinAnnotationColorPurple;
customPinView.animatesDrop = YES;
customPinView.canShowCallout = YES;
// 添加右边的详情按钮。
UIButton *rightButton = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
// 因为没有页面跳转,所以Target和action参数设为nil。
[rightButton addTarget:nil action:nil forControlEvents:UIControlEventTouchUpInside];
customPinView.rightCalloutAccessoryView = rightButton;
// 在callout左边添加自定义图片。
UIImageView *myCustomImage = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"MyCustomImage.png"]];
customPinView.leftCalloutAccessoryView = myCustomImage;
return customPinView;
}
在iOS开发中,实现mapView:annotationView:calloutAccessoryControlTapped:
代理方法来响应callout的control(必须是继承自UIControl
)的点击事件。在实现这个方法的时候,通过AnnotationView的identifier
来分别那个AnnotationView的callout的control被点击了。
当自定义callout时,需要多做一些工作,保证callout能够正常显示和消失。
创建一个
UIView
的子类。需要重写drawRect:
方法。创建一个ViewController,初始化callout,执行按钮的点击事件。
在AnnotationView中,实现
hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
方法响应在callout边界内的点击事情。在AnnotationView中,实现
setSelected:animated:
方法。将自定义的callout作为annotationView的subView。当用户点击annotationView时,显示callout。如果callout已经显示,那么在setSelected:animated:
中应该让callout消失,并从subViews
中移除。在annotationView的
initWithAnnotation:
方法中,将canShowCallout
设为NO
,防止用户点击annotationView弹出系统的callout。
显示多个annotationView
上文提及了显示多个annotationView会造成的不良后果。在缩放程度过小的时候,多个annotationView会因为离得太近而乱成一堆,用户无法清晰地分辨。而解决的方案就是通过缩放比例,改变显示的annotationView的个数。
调用mapView:regionWillChangeAnimated:
和mapView:regionDidChangeAnimated:
方法检测缩放程度。当它变化时,根据需要添加或移除一部分annotationView。也许,还需要考虑其他因素(比如用户当前位置)决定它们的去留。
添加Overlays
Overlays可以让我们在地图上标记处任意的区域。和Annotations不同,Overlays是根据多个坐标定义的。根据这些坐标,可以将它们连成线、矩形、圆或者其他不规则的图形,同时可以给这些图形填充颜色。利用Overlays,我们可以显示路况信息、地点的边界、路线等等。
和显示Annotation一样,显示Overlays同样要定义两个对象:
overlay object。遵循MKOverlay协议,管理overlay相关的坐标点。
overlayRender。MKOverlayRender类的对象,用来定义显示在地图上的overlay。
__在iOS 7.0之后,使用MKOverlayRender代替MKOverlayView。前者提供了和MKOverlayView相同的功能,但更加轻量、高效。
MKOverlay和MKOverlayRender类的作用,请对比MKAnnotation和MKAnnotationView。
添加Overlays的步骤
-
定义一个MKOverlay对象。
直接使用
MKCircle
、MKPolygon
或者MKPolyline
类。继承
MKShape
或者MKMultiPoint
类。使用任何遵循
MKOverlay
协议的类。
-
定义Overlay Render。
对一些标准的形状,比如圆形、多边形等,使用
MKCircleRender
、MKPolygonRender
或者MKPolylineRender
。通过设置这些类的属性可以得到不同的样式。对于继承
MKShape
的自定义形状,定义一个MKOverlayPathRender
的子类呈现它们。对于其他自定义的overlay,定义
MKOverlayRenderer
类的子类,实现自己的绘制方法。
实现
mapView
的mapView:rendererForOverlay:
代理方法。使用
addOverlay:
方法,将其添加到mapView上。
和annotation不同的是,overlay会随着地图的缩放而缩放。因为overlay表示地图上的边界、路线等信息。
使用标准的Overlays对象和View
如果想标注显示地图上的某个区域,使用标准的Overlay类是最简单的方法。标准的Overlay类包括MKCircle
、MKPolypon
、MKPolyline
,配合MKCircleRender
、MKPolygonRender
、MKPolylineRender
类将它们显示到mapView上。
定义一个MKPolyline对象。MKPolyline有两个初始化方法,分别是使用CLLocationCoordinate
类型的数组
和MKMapPoint
类型的数组
,count
参数表示数组中所包含的元素个数。
CLLocationCoordinate2D points[2];
points[0] = CLLocationCoordinate2DMake(30.000000, 120.000000);
points[1] = CLLocationCoordinate2DMake(40.000000, 130.000000);
MKPolyline *polyline = [MKPolyline polylineWithCoordinates:points count:2];
[self.mapView addOverlay:polyline];
要把overlay显示到mapView上,必须实现mapView的mapView:rendererForOverlay:
代理方法,返回MKOverlayRender
类的对象。
- (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id)overlay {
if ([overlay isKindOfClass:[MKPolyline class]]) {
MKPolylineRenderer *polylineRender = [[MKPolylineRenderer alloc] initWithPolyline:(MKPolyline *)overlay];
[polylineRender setNeedsDisplay];
polylineRender.fillColor = [UIColor redColor];
polylineRender.strokeColor = [UIColor redColor];
polylineRender.lineWidth = 1.0f;
return polylineRender;
}
return nil;
}
注意:如果是用MKPolylineRender的话,使用strokeColor
属性设置其颜色,而不是fillColor
属性。
关于搜索
通过MKLocalSearch
、MKLocalSearchRequest
,我们可以实现对地图的搜索。
先来一段代码。结合UISearchBar和MKMapKit,将搜索的内容在地图上标注出来。在每次重新搜索的时候移除已经添加的MKAnnotation。搜索的结果以MKMapItem
类型的数组给出,可以取出位置相关的信息,比如名称,经纬度,url等等。
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
[self.mapView removeAnnotations:self.annotationArray];
self.searchRequest.naturalLanguageQuery = self.searchBar.text;
self.localSearch = [[MKLocalSearch alloc] initWithRequest:self.searchRequest];
[self.localSearch startWithCompletionHandler:^(MKLocalSearchResponse * _Nullable response, NSError * _Nullable error) {
self.resultArray = [NSMutableArray arrayWithArray:response.mapItems];
self.annotationArray = [NSMutableArray array];
for (MKMapItem *mapItem in self.resultArray) {
self.placeAnnotation = [[PlaceAnnotation alloc] init];
self.placeAnnotation.coordinate = mapItem.placemark.location.coordinate;
self.placeAnnotation.title = mapItem.name;
self.placeAnnotation.url = mapItem.url;
[self.annotationArray addObject:self.placeAnnotation];
[self.mapView addAnnotation:self.placeAnnotation];
}
}];
[self.searchBar resignFirstResponder];
[self.resultArray removeAllObjects];
}
结语
结合之前的两篇文章,我自己算是把MapKit和CoreLocation的基本用法理了一遍。从看官方文档到从Stackoverflow查找问题解决方法,花了挺长时间的。
最后总结一下使用MapKit和CoreLocation时需要注意的点:
-
如果App中需要用到定位服务,
首先要添加MapKit和CoreLocation这两个系统框架
其次根据需要在info.plist中加入
NSLocationWhenInUseUsageDescription
和NSLocationAlwaysUsageDescription
两个字段最后就是相应地调用
requestWhenInUseAuthorization
或者requestAlwaysAuthorization
方法请求用户授权。
为了保险起见,在每一次调用
startUpdatingLocation
方法前,检查App是否已经获取到定位服务的权限。使用定位服务是很耗电的,所以每次在退出有mapView的页面之前,调用一次
stopUpdatingLocation
,可能对节省电量有帮助。在使用MKPolylineRender时,使用
strokeColor
属性给其设置颜色。而不是fillColor
。
目前就写这么多,欢迎大家提意见和建议。
参考文档
Annotating Maps