讓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

相關文章
相關標籤/搜索