今天要講的是設備的位置,包括如何找到設備的位置和如何在地圖上顯示位置。
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