1

在MKMapView上添加标注可以方便用户更好地获取信息,与地图进行交互。标注分为两种,一种是Annotations,一种是Overlays

  • Annotations。标注由经纬度所确定的一个点,比如用户当前位置,一个被指定的地址,或者一个被收藏的地点。

  • Overlays。标注由多点连成的线,一个或者多个相邻或不相邻的区域。比如路线、交通状况、或者某个地点的边界。

和MKMapView中的subView不同,Annotations和Overlays会随着地图的移动而移动。

添加Annotations

Annotations可以是地图上一个点醒目地标注出来,并且可以提供这个地点一些简单的信息。你可以用Annotations来标注当前位置、指定的位置、或者被收藏的位置等等。可以在地图上用一组图片来分别标记这些位置,还可以通过calloutView显示基本信息和可操作的空间,比如链接到更详细的介绍页面。

下面这张图,使用了大头针来标注一个指定的地点,并且通过calloutView显示一些基本信息,以及一个点击后可以提供驾车导航的按钮点击后可以跳转获取更多信息的按钮

图1-1 大头针标注

如果要定义一个Annotation,要通过下面两个类:

  • Annotation object ,遵循MKAnnotation协议,管理Annotation相关的属性。

  • Annotation ViewMKAnnotationView类型,来绘制Annotation的样式。

MapKit已经提供一些标准样式的Annotations,比如上图所示的大头针。也可以自定义annotationView。无论是使用标准的还是自定义的annotationView,你都不能使用addSubView:的方法将他们添加到mapView上。而应该使用mapView的代理方法mapView:viewForAnnotation:

添加Annotations的步骤

按照以下步骤来实现和使用Annotations。假定已经添加了mapView。

  1. 用下面任意一种方法定义_Annotation object_。

    • MKPointAnnotation类实现一个简单的Annotation。用这个方法定义的Annotation object包含calloutView的title和subtitle属性。

    • 自定义一个遵循MKAnnotation协议的类。这个类可以包含任何你想包含的属性。

  2. 定义一个_Annotation View_。根据你的需要选择合适的方法。

    • 如果使用系统提供的大头针作为标注,只需要创建一个MKPinAnnotationView的实例即可。

    • 如果使用一张静态图片,创建一个MKAnnotationView的实例,给它的image属性赋值即可。

    • 如果上面两种方法已经无法满足你,那么就新建一个继承自MKAnnotation类的子类,实现绘制代码。

  3. 实现mapView的代理方法mapView:viewForAnnotation:

    在实现这个方法的时候,如果存在可以复用的annotationView,就直接使用。如果不存在,新建一个annotationView。如果需要显示多种类型的annotationView,根据annotaion类型不同,显示相应类型的annotationView。

    这个方法让我想起tableView:cellForRowAtIndexPath:。它们两个的实现方式很相似。

  4. 使用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 <MKAnnotation> {
    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<MKAnnotation>)annotation reuseIdentifier:(NSString *)reuseIdentifier;方法。

- (instancetype)initWithAnnotation:(id<MKAnnotation>)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 <MKAnnotation>)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中添加了图片和详情按钮。

图1-2 在calloutView两侧添加自定义View

接下来给出代码,如何实现修改callout样式。

// 假设这个annotationView已经添加到mapView上了。

- (MKAnnotationView *)mapView:(MKMapView *)theMapView viewForAnnotation:(id <MKAnnotation>)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能够正常显示和消失。

  1. 创建一个UIView的子类。需要重写drawRect:方法。

  2. 创建一个ViewController,初始化callout,执行按钮的点击事件。

  3. 在AnnotationView中,实现hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;方法响应在callout边界内的点击事情。

  4. 在AnnotationView中,实现setSelected:animated:方法。将自定义的callout作为annotationView的subView。当用户点击annotationView时,显示callout。如果callout已经显示,那么在setSelected:animated:中应该让callout消失,并从subViews中移除。

  5. 在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的步骤

  1. 定义一个MKOverlay对象。

    • 直接使用MKCircleMKPolygon或者MKPolyline类。

    • 继承MKShape或者MKMultiPoint类。

    • 使用任何遵循MKOverlay协议的类。

  2. 定义Overlay Render。

    • 对一些标准的形状,比如圆形、多边形等,使用MKCircleRenderMKPolygonRender或者MKPolylineRender。通过设置这些类的属性可以得到不同的样式。

    • 对于继承MKShape的自定义形状,定义一个MKOverlayPathRender的子类呈现它们。

    • 对于其他自定义的overlay,定义MKOverlayRenderer类的子类,实现自己的绘制方法。

  3. 实现mapViewmapView:rendererForOverlay:代理方法。

  4. 使用addOverlay:方法,将其添加到mapView上。

和annotation不同的是,overlay会随着地图的缩放而缩放。因为overlay表示地图上的边界、路线等信息。

使用标准的Overlays对象和View

如果想标注显示地图上的某个区域,使用标准的Overlay类是最简单的方法。标准的Overlay类包括MKCircleMKPolyponMKPolyline,配合MKCircleRenderMKPolygonRenderMKPolylineRender类将它们显示到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<MKOverlay>)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属性。

关于搜索

通过MKLocalSearchMKLocalSearchRequest,我们可以实现对地图的搜索。

先来一段代码。结合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中加入NSLocationWhenInUseUsageDescriptionNSLocationAlwaysUsageDescription两个字段

    • 最后就是相应地调用requestWhenInUseAuthorization或者requestAlwaysAuthorization方法请求用户授权。

  • 为了保险起见,在每一次调用startUpdatingLocation方法前,检查App是否已经获取到定位服务的权限。

  • 使用定位服务是很耗电的,所以每次在退出有mapView的页面之前,调用一次stopUpdatingLocation,可能对节省电量有帮助。

  • 在使用MKPolylineRender时,使用strokeColor属性给其设置颜色。而不是fillColor

目前就写这么多,欢迎大家提意见和建议。

参考文档

Annotating Maps


已注销
257 声望8 粉丝

毕业于淮海工学院网络工程系。