您的位置:首页 > 移动开发 > IOS开发

iOS应用开发视频教程笔记(十一)Core Location and Map Kit

2013-03-09 23:03 771 查看
今天要讲的是设备的位置,包括如何找到设备的位置和如何在地图上显示位置。

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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: