让MKMapView变得丰富多彩

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

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

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

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

添加Annotations

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

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

图1-1 大头针标注

若是要定义一个Annotation,要经过下面两个类:atom

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

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

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

相关文章
相关标签/搜索