iOS應用開發視頻教程筆記(十一)Core Location and Map Kit


今天要講的是設備的位置,包括如何找到設備的位置和如何在地圖上顯示位置。

Core Location

Core Location不是一個UI的東西,沒有用戶界面,它只是關於找到該設備的位置。新設備有很多定位裝置,比如磁力計、加速度計、全球定位系統(GPS),各種無線,各種能找出你在哪里的東西。Core Location的基本對象是一個CLLocation,CL是Core Location庫的前綴,location是基本對象。CLLocation里的properties:

@properties: coordinate, altitude, horizontal/verticalAccuracy, timestamp, speed, course

關於這個位置讀數的精度,會談到時間戳(timestamp),就是這個位置何時被記錄。speed,移動的速度有多快,通過GPS坐標的瞬時讀數判斷。course,類似移動的航行。最重要的是coordinate,它告訴你這個CLLocation在哪里。

coordinate是一個C結構體,只有latitude(經度)和longitude(緯度):

@property (readonly) CLLocationCoordinate2D coordinate; 
typedef {
      CLLocationDegrees latitude;       //a double
      CLLocationDegrees longitude;      //a double 
} CLLocationCoordinate2D;

@property (readonly) CLLocationDistance altitude; // meters

CLLocationDegrees基本上是double,浮點數。altitude(海拔)單位是米,可以得到海拔的高度。

coordinate的latitude和longitude有多精確?CLLocation有一對properties:

@property (readonly) CLLocationAccuracy horizontalAccuracy;    // in meters 
@property (readonly) CLLocationAccuracy verticalAccuracy;    // in meters

它告訴你漂移量是多少,單位是米。通常不會把它們當做數值來看,會更多地看這些預定義的值,如AccuracyBestForNavigation,它將不斷更新GPS。GPS可以測量垂直。

kCLLocationAccuracyNearestTenMeters就基本是你在的位置;kCLLocationAccuracyHundredMeters就是你在這一區域的某處;kCLLocationAccuracyKilometer和kCLLocationAccuracyThreeKilometers都是采用基站的方式,精度非常粗糙。

最好的是用不斷更新的GPS(精度最高,耗電量最大),次好的是用WiFi(設備可以發現周圍的WiFi熱點和信號有多強,並通過查看互聯網上的一個數據庫來找出你在哪里),第三個也是最不准確的是基站(最不准確的,幾乎不耗電)。所以是根據你的app需要什么精度來選擇。

速度、航向、時間戳,這些都是一種即時測量,比如speed只計算最后一定數量的GPS位置,course就是航向,所有這些API都是抽象的,它只是根據你的設備來向你匯報最佳信息。

怎么得到CLLocation,這些Core Location里的位置對象?幾乎總是通過另一個對象CLLocationManager獲取。

在ios 5中要注意的一件重要的事情是,你可以模擬你的位置,當你在模擬器上使用這個運行時才出現的菜單,基本上可以在地球上隨便挑個地方作為你的位置,甚至可以有自己的GPX文件,它像個簡單的XML格式,里面編碼了一堆經度和緯度,甚至可以模擬移動之類的:

有四件事與CLLocationManager有關,四個都要做:

第一個,是你要檢查有什么硬件可用;

第二個,你要創建這些CLLocationManagers之一,並設置自己為delegate,因為CLLocationManager將要使用delegate來進行更新;

第三個,你要配置這個manager,你想要什么樣的位置更新、航行或僅僅是位置移動,有各種不同的定位監測,它不是只能告訴我我在哪里, 它有相當的靈活性,當你設置好后要進行下一步;

第四個,就是你要開始監測,當你啟動它的監測,它會根據你的配置來給delegate發消息。

有哪些基於位置的監控?有基於精度的持續更新,這意味着你設置了精度,然后在這個精度等級下的移動都會得到持續的更新;再有就是只有發生顯著變化時才更新;還有基於區域的更新,定義一個地球上的區域,當人進入該地區時,你會得到更新;當然還有航行監測,它只會在設備指向不同的方向時進行報告。

第一個步驟是檢查,看看你的硬件可以做什么:

+(BOOL)locationServicesEnabled; //has the user enabled location monitoring in Settings? 
+(BOOL)headingAvailable;    // can this hardware provide heading info (compass)? 
+(BOOL)significantLocationChangeMonitoringAvailable; //only if device has cellular? 
+(BOOL)regionMonitoringAvailable;//only certain iOS4 devices 
+(BOOL)regionMonitoringEnabled;//by the user in Settings

你必須要檢查一些事情,比如locationServicesEnabled,因為當你請求這些東西的時候,系統會彈出警告說這個app要使用你的位置,如果用戶選擇no,定位服務不會開啟。甚至要檢查,看看這些東西是否可行,並非所有的設備可以做這些事情。

當系統提示你的app要使用位置服務的時候,它會用這個字符串,CLLocationManager里的Purpose string:

@property (copy) NSString *purpose;

這就是提示的內容,所以這會用來解釋為什么這個app要使用你的位置服務,這樣的目的可能是用你的GPS位置來標記你的圖片。你設置了CLLocationManager里的Purpose string,當你開始監控,如果系統詢問用戶,將會使用此字符串。如果終端用戶允許你使用定位服務,幾乎肯定可以得到他們的GPS定位。

在蘋果公司有很多政策文件,它們建議你如何做用戶界面,甚至是在App Store審核的時候的一些強制條款。

一旦檢查過了有什么硬件可用,現在你就可以從CLLocationManager獲取信息了,你通常不會主動獲取,通常你可以設置它的delegate,然后設置你想要的精度,然后你還可以設置distanceFilter,只有離開上個更新點一定距離才進行更新:

@property CLLocationAccuracy desiredAccuracy; //always set this as low as possible
@property CLLocationDistance distanceFilter;

你設置了這些,然后你只需要調用CLLocationManager的startUpdatingLocation,它將開始向你的delegate發消息:

- (void)startUpdatingLocation; 
- (void)stopUpdatingLocation;

最主要的方法是:

- (void)locationManager:(CLLocationManager *)manager 
didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation;

它會向你發送一個新的CLLocation對象,也將發送給你上次那個,這樣你就可以跟蹤用戶在做什么。

Heading monitoring基本上和location monitoring是一樣的,只是監測航向。當它給你一個航向,heading長什么樣子?就像CLLocation一樣,是CLHeading,CLHeading有magneticHeading(磁場航向)和trueHeading(真實航向),如果位置服務被關閉了,GPS和wifi所有這一切都是通過磁力計的,都是magneticHeading。位置服務必須打開,否則無法得到trueHeading,只能得到magneticHeading。

磁力計會讓你把手機做8字移動,這樣它就能測量磁力計收到的電磁干擾,這是由ios自動完成。可以通過在以下方法里返回NO來阻止它:

- (BOOL)locationManagerShouldDisplayHeadingCalibration:(CLLocationManager *)manager;

你不應該忽略此delegate方法,不能得到很好的航向信息,就會得到error,就會知道發生了什么事:

- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error;

這是基於精度的更新,還有其他各種位置監測,一個被稱為Significant location change monitoring(顯著位置變化監測),它只監視大的變化,酷的是你的app甚至不用運行,一旦你的app注冊以后,你就會開始接收這些顯著的位置變化。如果app因為Significant location change而被運行,你會得到appdelegate.m中的application:didFinishLaunchingWithOptions:方法。如果你打開這個Significant location change,然后Significant location change發生了,並且app在后台,app仍然會得到其delegate方法,app會得到喚醒。

類似的,還有基於區域的東西,指定一個區域,當進入或離開該區域,你都會得到更新。同樣的,沒有運行也會得到這些更新。

Map Kit

這是一個不同的framework,這是你如何使用這種谷歌地圖技術來圖形地顯示位置。

Map Kit里的主要類是MKMapView,它只是一個UIView,用來顯示地圖。在地圖上,MKMapView有一個非常重要的property稱為annotations,它是一個實現MKAnnotation protocol的對象的NSArray,該protocol僅僅是一個coordinate、一個title和一個subtitle。annotations使用MKAnnotationView來顯示在地圖上,它們以紅色的pins的形式出現,annotations還可以有一個callout,當你點擊pin,一種灰色的矩形會出現就是callout,並展示了一些信息,比如它要同時呈現title和subtitle,也可以有左右callout accessory。

MKMapView

要如何創建一個MapView?通常將它從對象庫里拖到你的東西里面。還有這個property,annotations的數組:

@property (readonly) NSArray *annotations; // contains id <MKAnnotation> objects

這是一個id<MKAnnotation>對象的數組,它們可以是任何類對象,但它們必須要實現MKAnnotation protocol。所以這不是delegation,這是一個不同的protocol使用,這僅是定義一個對象可以做什么。

MKAnnotation protocol里有什么?其中之一是required的,就是coordinate,這只是一個CLLocationCoordinate2D;其他兩個是optional:title和subtitle。annotation里的對象只要實現這幾個。

注意annotations是只讀的,可以通過以下方法來添加annotations或刪除annotations:

- (void)addAnnotation:(id <MKAnnotation>)annotation; 
- (void)addAnnotations:(NSArray *)annotations; 
- (void)removeAnnotation:(id <MKAnnotation>)annotation; 
- (void)removeAnnotations:(NSArray *)annotations;

通常一開始你就把你的annotations都放進去,因為MapView會像TableView那樣重用這些pins。如果把它們都放進去,就會知道所有pins的位置,可以更高效的知道當前哪些地方需要pins。

annotations在地圖上的外觀:這些pins,這些MKPinAnnotationView,是在Map Kit里的默認的view,它有幾個property,比如可以設置pins的顏色。也可以創建自己的MKAnnotationView子類,基礎類是image,MKPinAnnotationView就是設置該image為pin,不要把MKAnnotation使用的image如這個pin image和callout里的image混淆了。

當你點擊一個annotation view會發生什么呢?callout會出來。如何控制callout的內容,在MapView里有個非常重要的delegate,設置它的delegate就會得到這個消息:

- (void)mapView:(MKMapView *)sender didSelectAnnotationView:(MKAnnotationView *)aView;

當annotation被點擊的時候它會發消息給你。記住,annotation view知道哪個annotation正在被瀏覽,所以也就可以得到被點擊的那個annotation。

如何創建這些MKAnnotationViews?如果什么都不做,會得到pin,當你點擊pin,會得到一個帶有title和subtitle的callout。如果你想控制callout的內容或外觀,你可以實現MapView的delegate方法:

- (MKAnnotationView *)mapView:(MKMapView *)sender
            viewForAnnotation:(id <MKAnnotation>)annotation
{
     MKAnnotationView *aView = [sender dequeueReusableAnnotationViewWithIdentifier:IDENT];
     if (!aView) {
         aView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:IDENT];
         // set canShowCallout to YES and build aView’s callout accessory views here
     }
     aView.annotation = annotation; // yes, this happens twice if no dequeue   
     // maybe load up accessory views here (if not too expensive)? 
     // or reset them and wait until mapView:didSelectAnnotationView: to load actual data 
     return aView;
}

這和tableView的cellForRowAtIndexPath非常相似。每次要在地圖上顯示一個特定的annotation的時候,它會被調用,它要調用這個來得到要顯示的veiw。要讓canShowCallout等於YES,否則點擊的時候就不會得到callout。不管aView是被創建或被出隊,都要設置annotations。

如果設置了callout的左側或右側為一個control,就像一個按鈕,當有人點擊它,就會調用MKMapViewDelegate的方法:

              - (void)mapView:(MKMapView *)sender 
               annotationView:(MKAnnotationView *)aView
calloutAccessoryControlTapped:(UIControl *)control;

因此這樣就不用再建target action,這是一個callout view內部的按鈕。不要把這個方法和didSelectAnnotationView弄混了,這是點擊在callout里的按鈕上,而后者是點擊在pin上。

通常直到didSelectAnnotationView發生了,才顯示callout里的左右測:

- (void)mapView:(MKMapView *)sender didSelectAnnotationView:(MKAnnotationView *)aView {
        if ([aView.leftCalloutAccessoryView isKindOfClass:[UIImageView class]]) {          
UIImageView *imageView = (UIImageView *)aView.leftCalloutAccessoryView; imageView.image=...; //if you do this in a GCD queue,be careful,views are reused! } }

不希望在viewForAnnotation里加載所有callout的圖片,要在didSelectAnnotationView里才加載。如果正在使用GCD加載Flickr的圖像的縮略圖,用了另一個線程,線程返回時要小心,因為pins是被重用的。pins可能在某個地方被點擊了,然后用戶滾動到其他地方了,當Flickr的東西下載回來的時候,所選擇的annotation view可能早就不在了,所以返回的時候得做一些檢查,當Flickr的圖像回來的時候得確保界面還是原來的樣子。所以要做個測試,以確保在寫image=之前一切仍然是老樣子。

配置display type:用MKMapType mapType來指定顯示衛星模式或街道模式和衛星模式的混合體。還可以用一個特殊的pin顯示用戶當前的位置,也可以在地圖上縮放和滾動:

@property MKMapType mapType;
@property BOOL showsUserLocation; 
@property (readonly) BOOL isUserLocationVisible; 
@property (readonly) MKUserLocation *userLocation;
@property BOOL zoomEnabled; 
@property BOOL scrollEnabled;

可以通過設置MapView的region property來控制顯示的區域,它只是個CLLocationCoordinate2D,這是經度、緯度和一個跨度,span(跨度)就是緯度上有多遠,center就是其中心,region有一定的跨度。當你設置了region,地圖將可以滾動和放大顯示該區域,也可以只設置中心點,這樣就只能滾動不能縮放。

@property MKCoordinateRegion region; 
typedef struct {
     CLLocationCoordinate2D center;
     MKCoordinateSpan span; 
} MKCoordinateRegion; 
typedef struct {
     CLLocationDegrees latitudeDelta; 
     CLLocationDegrees longitudeDelta;
}
- (void)setRegion:(MKCoordinateRegion)region animated:(BOOL)animated; // animate

開始加載地圖時,delegate會得到通知。記住,整個世界、衛星圖像都不在手機上,所以當你左右滾動,它從Google Map上下載地圖信息,因此它是一塊一塊顯示的。這將告訴你什么時候開始向網絡獲取更多,和何時它完成加載。

Remember that the maps are downloaded from Google earth.
- (void)mapViewWillStartLoadingMap:(MKMapView *)sender; 
- (void)mapViewDidFinishLoadingMap:(MKMapView *)sender; 
- (void)mapViewDidFailLoadingMap:(MKMapView *)sender withError:(NSError *)error;

Overlays

它和annotations非常相似,不同的地方只是繪制圖像、點擊一下,可以繪制Overlays。通常情況下,Overlays更大,它們不是在一個點上,它們將是Overlays的重疊,你要實際繪制它。

設置Overlays的方法和annotations一樣:

- (void)addOverlay:(id <MKOverlay>)overlay;    // also addOverlays:(NSArray *) 
- (void)removeOverlay:(id<MKOverlay>)overlay; //alsoremoveOverlays:(NSArray*)

也有和cellForRowAtIndexPath或viewForAnnotation相同的機制:

- (MKOverlayView *)mapView:(MKMapView *)sender
            viewForOverlay:(id <MKOverlay>)overlay;

MKOverlayView基本上就是實現了類似drawRect的東西,它不叫drawRect,而是:

- (void)drawMapRect:(MKMapRect)mapRect 
          zoomScale:(MKZoomScale)zoomScale 
          inContext:(CGContextRef)context;

不調用UIGraphics獲得context,它給你一個context來做CoreGraphics的繪制。

Demo

要用上次的shutterbug,把它加入Spli View,details一側是master里對應項的地圖。

從對象庫中拖出一個Spli View,並刪除它的master,用control+drag的方式指定新的master。重新創建一個UIViewController的子類叫MapViewController,設置detail的類為MapViewController,再對象庫中拖一個Map View到detail中,然后為mapView創建一個outlet到MapViewController,這時會出現一個紅色的error,這是因為MKMapView所屬的framework還沒有鏈接到這個app。那么要怎么解決呢?回到project navigator,點擊project,再點擊target,然后去到build phases,可以看到link binary with libraries,這就是添加framework的地方,選擇MapKit.framework和CoreLocation.framework。回到MapViewController,#import <MapKit/MapKit.h>,紅色的error就會消失。

在MapViewController.h文件中創建一個model,是annotations數組,最終是要把這些東西傳遞給Map View。但因為Map View不是public的,所以需要有一些公共的API。

對FlickrPhotoTableViewController.m文件添加了以下一些代碼:

- (NSArray *)mapAnnotations
{
    NSMutableArray *annotations = [NSMutableArray arrayWithCapacity:[self.photos count]];
    for (NSDictionary *photo in self.photos) {
        [annotations addObject:[FlickrPhotoAnnotation annotationForPhoto:photo]];
    }
    return annotations;
}

- (void)updateSplitViewDetail
{
    id detail = [self.splitViewController.viewControllers lastObject];
    if ([detail isKindOfClass:[MapViewController class]]) {
        MapViewController *mapVC = (MapViewController *)detail;
        mapVC.delegate = self;
        mapVC.annotations = [self mapAnnotations];
    }
}

- (void)setPhotos:(NSArray *)photos
{
    if (_photos != photos) {
        _photos = photos;
        [self updateSplitViewDetail];
        // Model changed, so update our View (the table)
        if (self.tableView.window) [self.tableView reloadData];
    }
}

創建一個實現了MKAnnotation protocol的NSOject的子類,將其命名為FlickrPhotoAnnotation,FlickrPhotoAnnotation.h文件代碼如下:

#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>

@interface FlickrPhotoAnnotation : NSObject <MKAnnotation>

+ (FlickrPhotoAnnotation *)annotationForPhoto:(NSDictionary *)photo; // Flickr photo dictionary

@property (nonatomic, strong) NSDictionary *photo;

@end

FlickrPhotoAnnotation.m文件代碼:

#import "FlickrPhotoAnnotation.h"
#import "FlickrFetcher.h"

@implementation FlickrPhotoAnnotation

@synthesize photo = _photo;

+ (FlickrPhotoAnnotation *)annotationForPhoto:(NSDictionary *)photo
{
    FlickrPhotoAnnotation *annotation = [[FlickrPhotoAnnotation alloc] init];
    annotation.photo = photo;
    return annotation;
}

#pragma mark - MKAnnotation

- (NSString *)title
{
    return [self.photo objectForKey:FLICKR_PHOTO_TITLE];
}

- (NSString *)subtitle
{
    return [self.photo valueForKeyPath:FLICKR_PHOTO_DESCRIPTION];
}

- (CLLocationCoordinate2D)coordinate
{
    CLLocationCoordinate2D coordinate;
    coordinate.latitude = [[self.photo objectForKey:FLICKR_LATITUDE] doubleValue];
    coordinate.longitude = [[self.photo objectForKey:FLICKR_LONGITUDE] doubleValue];
    return coordinate;
}

@end

當你有一個通用類,想讓它從另一個類獲取數據的時候,你要怎么做?子類或delegation。

MapViewController.h文件代碼:

#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>

@class MapViewController;

@protocol MapViewControllerDelegate <NSObject>
- (UIImage *)mapViewController:(MapViewController *)sender imageForAnnotation:(id <MKAnnotation>)annotation;
@end

@interface MapViewController : UIViewController
@property (nonatomic, strong) NSArray *annotations; // of id <MKAnnotation>
@property (nonatomic, weak) id <MapViewControllerDelegate> delegate;
@end

MapViewController.m文件代碼:

#import "MapViewController.h"

@interface MapViewController() <MKMapViewDelegate>
@property (weak, nonatomic) IBOutlet MKMapView *mapView;
@end

@implementation MapViewController

@synthesize mapView = _mapView;
@synthesize annotations = _annotations;
@synthesize delegate = _delegate;

#pragma mark - Synchronize Model and View

- (void)updateMapView
{
    if (self.mapView.annotations) [self.mapView removeAnnotations:self.mapView.annotations];
    if (self.annotations) [self.mapView addAnnotations:self.annotations];
}

- (void)setMapView:(MKMapView *)mapView
{
    _mapView = mapView;
    [self updateMapView];
}

- (void)setAnnotations:(NSArray *)annotations
{
    _annotations = annotations;
    [self updateMapView];
}

#pragma mark - MKMapViewDelegate

- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation
{
    MKAnnotationView *aView = [mapView dequeueReusableAnnotationViewWithIdentifier:@"MapVC"];
    if (!aView) {
        aView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:@"MapVC"];
        aView.canShowCallout = YES;
        aView.leftCalloutAccessoryView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 30, 30)];
        // could put a rightCalloutAccessoryView here
    }

    aView.annotation = annotation;
    [(UIImageView *)aView.leftCalloutAccessoryView setImage:nil];
    
    return aView;
}

- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)aView
{
    UIImage *image = [self.delegate mapViewController:self imageForAnnotation:aView.annotation];
    [(UIImageView *)aView.leftCalloutAccessoryView setImage:image];
}

- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control
{
    NSLog(@"callout accessory tapped for annotation %@", [view.annotation title]);
}

#pragma mark - View Controller Lifecycle

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.mapView.delegate = self;
}

- (void)viewDidUnload
{
    [self setMapView:nil];
    [super viewDidUnload];
}

#pragma mark - Autorotation

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    return YES;
}

@end

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM