概览
现在很多社交、电商、团购应用都引入了地图和定位功能,似乎地图功能不再是地图应用和导航应用所特有的。的确,有了地图和定位功能确实让我们的生活更加丰富多彩,极大的改变了我们的生活方式。例如你到了一个陌生的地方想要查找附近的酒店、超市等就可以打开软件搜索周边;类似的,还有很多团购软件可以根据你所在的位置自动为你推荐某些商品。总之,目前地图和定位功能已经大量引入到应用开发中。今天就和大家一起看一下iOS如何进行地图和定位开发。
定位
要实现地图、导航功能,往往需要先熟悉定位功能,在iOS中通过Core Location框架进行定位操作。Core Location自身可以单独使用,和地图开发框架MapKit完全是独立的,但是往往地图开发要配合定位框架使用。在Core Location中主要包含了定位、地理编码(包括反编码)功能。
定位功能
定位是一个很常用的功能,如一些地图软件打开之后如果用户允许软件定位的话,那么打开软件后就会自动锁定到当前位置,如果用户手机移动那么当前位置也会跟随着变化。要实现这个功能需要使用Core Loaction中CLLocationManager类,首先看一下这个类的一些主要方法和属性:
类方法 | 说明 |
+ (BOOL)locationServicesEnabled; | 是否启用定位服务,通常如果用户没有启用定位服务可以提示用户打开定位服务 |
+ (CLAuthorizationStatus)authorizationStatus; | 定位服务授权状态,返回枚举类型: kCLAuthorizationStatusNotDetermined: 用户尚未做出决定是否启用定位服务 kCLAuthorizationStatusRestricted: 没有获得用户授权使用定位服务,可能用户没有自己禁止访问授权 kCLAuthorizationStatusDenied :用户已经明确禁止应用使用定位服务或者当前系统定位服务处于关闭状态 kCLAuthorizationStatusAuthorizedAlways: 应用获得授权可以一直使用定位服务,即使应用不在使用状态 kCLAuthorizationStatusAuthorizedWhenInUse: 使用此应用过程中允许访问定位服务 |
属性 | 说明 |
desiredAccuracy | 定位精度,枚举类型: kCLLocationAccuracyBest:最精确定位 |
distanceFilter | 位置信息更新最小距离,只有移动大于这个距离才更新位置信息,默认为kCLDistanceFilterNone:不进行距离限制 |
对象方法 | 说明 |
startUpdatingLocation | 开始定位追踪,开始定位后将按照用户设置的更新频率执行-(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations;方法反馈定位信息 |
stopUpdatingLocation | 停止定位追踪 |
startUpdatingHeading | 开始导航方向追踪 |
stopUpdatingHeading | 停止导航方向追踪 |
startMonitoringForRegion: | 开始对某个区域进行定位追踪,开始对某个区域进行定位后。如果用户进入或者走出某个区域会调用- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region和- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region代理方法反馈相关信息 |
stopMonitoringForRegion: | 停止对某个区域进行定位追踪 |
requestWhenInUseAuthorization | 请求获得应用使用时的定位服务授权,注意使用此方法前在要在info.plist中配置NSLocationWhenInUseUsageDescription |
requestAlwaysAuthorization | 请求获得应用一直使用定位服务授权,注意使用此方法前要在info.plist中配置NSLocationAlwaysUsageDescription |
代理方法 | 说明 |
-(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations; | 位置发生改变后执行(第一次定位到某个位置之后也会执行) |
- (void)locationManager:(CLLocationManager *)manager didUpdateHeading:(CLHeading *)newHeading; | 导航方向发生变化后执行 |
- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region | 进入某个区域之后执行 |
- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region | 走出某个区域之后执行 |
iOS 8 还提供了更加人性化的定位服务选项。App 的定位服务不再仅仅是关闭或打开,现在,定位服务的启用提供了三个选项,「永不」「使用应用程序期间」和「始终」。同时,考虑到能耗问题,如果一款 App 要求始终能在后台开启定位服务,iOS 8 不仅会在首次打开 App 时主动向你询问,还会在日常使用中弹窗提醒你该 App 一直在后台使用定位服务,并询问你是否继续允许。在iOS7及以前的版本,如果在应用程序中使用定位服务只要在程序中调用startUpdatingLocation方法应用就会询问用户是否允许此应用是否允许使用定位服务,同时在提示过程中可以通过在info.plist中配置通过配置Privacy - Location Usage Description告诉用户使用的目的,同时这个配置是可选的。
但是在iOS8中配置配置项发生了变化,可以通过配置NSLocationAlwaysUsageDescription或者NSLocationWhenInUseUsageDescription来告诉用户使用定位服务的目的,并且注意这个配置是必须的,如果不进行配置则默认情况下应用无法使用定位服务,打开应用不会给出打开定位服务的提示,除非安装后自己设置此应用的定位服务。同时,在应用程序中需要根据配置对requestAlwaysAuthorization或locationServicesEnabled方法进行请求。由于本人机器已经更新到最新的iOS8.1下面的内容主要针对iOS8,使用iOS7的朋友需要稍作调整。
// // KCMainViewController.m // CoreLocation //#import "KCMainViewController.h" #import <CoreLocation/CoreLocation.h>@interface KCMainViewController ()<CLLocationManagerDelegate>{CLLocationManager *_locationManager; }@end@implementation KCMainViewController- (void)viewDidLoad {[super viewDidLoad];//定位管理器_locationManager=[[CLLocationManager alloc]init];if (![CLLocationManager locationServicesEnabled]) {NSLog(@"定位服务当前可能尚未打开,请设置打开!");return;}//如果没有授权则请求用户授权if ([CLLocationManager authorizationStatus]==kCLAuthorizationStatusNotDetermined){[_locationManager requestWhenInUseAuthorization];}else if([CLLocationManager authorizationStatus]==kCLAuthorizationStatusAuthorizedWhenInUse){//设置代理_locationManager.delegate=self;//设置定位精度_locationManager.desiredAccuracy=kCLLocationAccuracyBest;//定位频率,每隔多少米定位一次CLLocationDistance distance=10.0;//十米定位一次_locationManager.distanceFilter=distance;//启动跟踪定位[_locationManager startUpdatingLocation];} }#pragma mark - CoreLocation 代理 #pragma mark 跟踪定位代理方法,每次位置发生变化即会执行(只要定位到相应位置) //可以通过模拟器设置一个虚拟位置,否则在模拟器中无法调用此方法 -(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations{CLLocation *location=[locations firstObject];//取出第一个位置CLLocationCoordinate2D coordinate=location.coordinate;//位置坐标NSLog(@"经度:%f,纬度:%f,海拔:%f,航向:%f,行走速度:%f",coordinate.longitude,coordinate.latitude,location.altitude,location.course,location.speed);//如果不需要实时定位,使用完即使关闭定位服务[_locationManager stopUpdatingLocation]; }@end
注意:
1.定位频率和定位精度并不应当越精确越好,需要视实际情况而定,因为越精确越耗性能,也就越费电。
2.定位成功后会根据设置情况频繁调用-(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations方法,这个方法返回一组地理位置对象数组,每个元素一个CLLocation代表地理位置信息(包含经度、纬度、海报、行走速度等信息),之所以返回数组是因为有些时候一个位置点可能包含多个位置。
3.使用完定位服务后如果不需要实时监控应该立即关闭定位服务以节省资源。
4.除了提供定位功能,CLLocationManager还可以调用startMonitoringForRegion:方法对指定区域进行监控。
地理编码
除了提供位置跟踪功能之外,在定位服务中还包含CLGeocoder类用于处理地理编码和逆地理编码(又叫反地理编码)功能。
地理编码:根据给定的位置(通常是地名)确定地理坐标(经、纬度)。
逆地理编码:可以根据地理坐标(经、纬度)确定位置信息(街道、门牌等)。
CLGeocoder最主要的两个方法就是- (void)geocodeAddressString:(NSString *)addressString completionHandler:(CLGeocodeCompletionHandler)completionHandler;和- (void)reverseGeocodeLocation:(CLLocation *)location completionHandler:(CLGeocodeCompletionHandler)completionHandler;,分别用于地理编码和逆地理编码。下面简单演示一下:
// // KCMainViewController.m // CoreLocation //#import "KCMainViewController.h" #import <CoreLocation/CoreLocation.h>@interface KCMainViewController ()<CLLocationManagerDelegate>{CLGeocoder *_geocoder; }@end@implementation KCMainViewController- (void)viewDidLoad {[super viewDidLoad];_geocoder=[[CLGeocoder alloc]init];[self getCoordinateByAddress:@"北京"];[self getAddressByLatitude:39.54 longitude:116.28]; }#pragma mark 根据地名确定地理坐标 -(void)getCoordinateByAddress:(NSString *)address{//地理编码[_geocoder geocodeAddressString:address completionHandler:^(NSArray *placemarks, NSError *error) {//取得第一个地标,地标中存储了详细的地址信息,注意:一个地名可能搜索出多个地址CLPlacemark *placemark=[placemarks firstObject];CLLocation *location=placemark.location;//位置CLRegion *region=placemark.region;//区域NSDictionary *addressDic= placemark.addressDictionary;//详细地址信息字典,包含以下部分信息 // NSString *name=placemark.name;//地名 // NSString *thoroughfare=placemark.thoroughfare;//街道 // NSString *subThoroughfare=placemark.subThoroughfare; //街道相关信息,例如门牌等 // NSString *locality=placemark.locality; // 城市 // NSString *subLocality=placemark.subLocality; // 城市相关信息,例如标志性建筑 // NSString *administrativeArea=placemark.administrativeArea; // 州 // NSString *subAdministrativeArea=placemark.subAdministrativeArea; //其他行政区域信息 // NSString *postalCode=placemark.postalCode; //邮编 // NSString *ISOcountryCode=placemark.ISOcountryCode; //国家编码 // NSString *country=placemark.country; //国家 // NSString *inlandWater=placemark.inlandWater; //水源、湖泊 // NSString *ocean=placemark.ocean; // 海洋 // NSArray *areasOfInterest=placemark.areasOfInterest; //关联的或利益相关的地标NSLog(@"位置:%@,区域:%@,详细信息:%@",location,region,addressDic);}]; }#pragma mark 根据坐标取得地名 -(void)getAddressByLatitude:(CLLocationDegrees)latitude longitude:(CLLocationDegrees)longitude{//反地理编码CLLocation *location=[[CLLocation alloc]initWithLatitude:latitude longitude:longitude];[_geocoder reverseGeocodeLocation:location completionHandler:^(NSArray *placemarks, NSError *error) {CLPlacemark *placemark=[placemarks firstObject];NSLog(@"详细信息:%@",placemark.addressDictionary);}]; }@end
地图
iOS从6.0开始地图数据不再由谷歌驱动,而是改用自家地图,当然在国内它的数据是由高德地图提供的。这样一来,如果在iOS6.0之前进行地图开发的话使用方法会有所不同,基于目前的情况其实使用iOS6.0之前版本的系统基本已经寥寥无几了,所有在接下来的内容中不会再针对iOS5及之前版本的地图开发进行介绍。
在iOS中进行地图开发主要有两种方式,一种是直接利用MapKit框架进行地图开发,利用这种方式可以对地图进行精准的控制;另一种方式是直接调用苹果官方自带的地图应用,主要用于一些简单的地图应用(例如:进行导航覆盖物填充等),无法进行精确的控制。当然,本节重点内容还是前者,后面的内容也会稍加提示。
用MapKit之前需要简单了解一下MapKit中地图展示控件MKMapView的的一些常用属性和方法,具体如下表:属性 | 说明 |
userTrackingMode | 跟踪类型,是一个枚举: MKUserTrackingModeNone :不进行用户位置跟踪; MKUserTrackingModeFollow :跟踪用户位置; MKUserTrackingModeFollowWithHeading :跟踪用户位置并且跟踪用户前进方向; |
mapType | 地图类型,是一个枚举: MKMapTypeStandard :标准地图,一般情况下使用此地图即可满足; MKMapTypeSatellite :卫星地图; MKMapTypeHybrid :混合地图,加载最慢比较消耗资源; |
userLocation | 用户位置,只读属性 |
annotations | 当前地图中的所有大头针,只读属性 |
对象方法 | 说明 |
- (void)addAnnotation:(id <MKAnnotation>)annotation; | 添加大头针,对应的有添加大头针数组 |
- (void)removeAnnotation:(id <MKAnnotation>)annotation; | 删除大头针,对应的有删除大头针数组 |
- (void)setRegion:(MKCoordinateRegion)region animated:(BOOL)animated; | 设置地图显示区域,用于控制当前屏幕显示地图范围 |
- (void)setCenterCoordinate:(CLLocationCoordinate2D)coordinate animated:(BOOL)animated; | 设置地图中心点位置 |
- (CGPoint)convertCoordinate:(CLLocationCoordinate2D)coordinate toPointToView:(UIView *)view; | 将地理坐标(经纬度)转化为数学坐标(UIKit坐标) |
- (CLLocationCoordinate2D)convertPoint:(CGPoint)point toCoordinateFromView:(UIView *)view; | 将数学坐标转换为地理坐标 |
- (MKAnnotationView *)dequeueReusableAnnotationViewWithIdentifier:(NSString *)identifier; | 从缓存池中取出大头针,类似于UITableView中取出UITableViewCell,为了进行性能优化而设计 |
- (void)selectAnnotation:(id <MKAnnotation>)annotation animated:(BOOL)animated; | 选中指定的大头针 |
- (void)deselectAnnotation:(id <MKAnnotation>)annotation animated:(BOOL)animated; | 取消选中指定的大头针 |
代理方法 | 说明 |
- (void)mapView:(MKMapView *)mapView didUpdateUserLocation:(MKUserLocation *)userLocation ; | 用户位置发生改变时触发(第一次定位到用户位置也会触发该方法) |
- (void)mapView:(MKMapView *)mapView didUpdateUserLocation:(MKUserLocation *)userLocation ; | 显示区域发生改变后触发 |
- (void)mapViewDidFinishLoadingMap:(MKMapView *)mapView; | 地图加载完成后触发 |
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation; | 显示大头针时触发,返回大头针视图,通常自定义大头针可以通过此方法进行 |
- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view | 点击选中某个大头针时触发 |
- (void)mapView:(MKMapView *)mapView didDeselectAnnotationView:(MKAnnotationView *)view | 取消选中大头针时触发 |
- (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id <MKOverlay>)overlay | 渲染地图覆盖物时触发 |
用户位置跟踪
在很多带有地图的应用中默认打开地图都会显示用户当前位置,同时将当前位置标记出来放到屏幕中点方便用户对周围情况进行查看。如果在iOS6或者iOS7中实现这个功能只需要添加地图控件、设置用户跟踪模式、在-(void)mapView:(MKMapView *)mapView didUpdateUserLocation:(MKUserLocation *)userLocation代理方法中设置地图中心区域及显示范围。但是在iOS8中用法稍有不同:
1.由于在地图中进行用户位置跟踪需要使用定位功能,而定位功能在iOS8中设计发生了变化,因此必须按照前面定位章节中提到的内容进行配置和请求。
2.iOS8中不需要进行中心点的指定,默认会将当前位置设置中心点并自动设置显示区域范围。
了解以上两点,要进行用户位置跟踪其实就相当简单了,值得一提的是-(void)mapView:(MKMapView *)mapView didUpdateUserLocation:(MKUserLocation *)userLocation这个代理方法。这个方法只有在定位(利用前面章节中的定位内容)到当前位置之后就会调用,以后每当用户位置发生改变就会触发,调用频率相当频繁。
大头针
在iOS开发中经常会标记某个位置,需要使用地图标注,也就是大家俗称的“大头针”。只要一个NSObject类实现MKAnnotation协议就可以作为一个大头针,通常会重写协议中coordinate(标记位置)、title(标题)、subtitle(子标题)三个属性,然后在程序中创建大头针对象并调用addAnnotation:方法添加大头针即可(之所以iOS没有定义一个基类实现这个协议供开发者使用,多数原因应该是MKAnnotation是一个模型对象,对于多数应用模型会稍有不同,例如后面的内容中会给大头针模型对象添加其他属性)。
KCAnnotation.h
// // KCAnnotation.h // MapKit //#import <Foundation/Foundation.h> #import <MapKit/MapKit.h>@interface KCAnnotation : NSObject<MKAnnotation>@property (nonatomic) CLLocationCoordinate2D coordinate; @property (nonatomic, copy) NSString *title; @property (nonatomic, copy) NSString *subtitle;@end
KCMainViewController.m
// // KCMainViewController.m // MapKit Annotation // 37.785834 -122.406417 // 39.92 116.39#import "KCMainViewController.h" #import <CoreLocation/CoreLocation.h> #import <MapKit/MapKit.h> #import "KCAnnotation.h"@interface KCMainViewController ()<MKMapViewDelegate>{CLLocationManager *_locationManager;MKMapView *_mapView; }@end@implementation KCMainViewController- (void)viewDidLoad {[super viewDidLoad];[self initGUI]; }#pragma mark 添加地图控件 -(void)initGUI{CGRect rect=[UIScreen mainScreen].bounds;_mapView=[[MKMapView alloc]initWithFrame:rect];[self.view addSubview:_mapView];//设置代理_mapView.delegate=self;//请求定位服务_locationManager=[[CLLocationManager alloc]init];if(![CLLocationManager locationServicesEnabled]||[CLLocationManager authorizationStatus]!=kCLAuthorizationStatusAuthorizedWhenInUse){[_locationManager requestWhenInUseAuthorization];}//用户位置追踪(用户位置追踪用于标记用户当前位置,此时会调用定位服务)_mapView.userTrackingMode=MKUserTrackingModeFollow;//设置地图类型_mapView.mapType=MKMapTypeStandard;//添加大头针[self addAnnotation]; }#pragma mark 添加大头针 -(void)addAnnotation{CLLocationCoordinate2D location1=CLLocationCoordinate2DMake(39.95, 116.35);KCAnnotation *annotation1=[[KCAnnotation alloc]init];annotation1.title=@"CMJ Studio";annotation1.subtitle=@"Kenshin Cui's Studios";annotation1.coordinate=location1;[_mapView addAnnotation:annotation1];CLLocationCoordinate2D location2=CLLocationCoordinate2DMake(39.87, 116.35);KCAnnotation *annotation2=[[KCAnnotation alloc]init];annotation2.title=@"Kenshin&Kaoru";annotation2.subtitle=@"Kenshin Cui's Home";annotation2.coordinate=location2;[_mapView addAnnotation:annotation2]; }#pragma mark - 地图控件代理方法 #pragma mark 更新用户位置,只要用户改变则调用此方法(包括第一次定位到用户位置) -(void)mapView:(MKMapView *)mapView didUpdateUserLocation:(MKUserLocation *)userLocation{NSLog(@"%@",userLocation);//设置地图显示范围(如果不进行区域设置会自动显示区域范围并指定当前用户位置为地图中心点)// MKCoordinateSpan span=MKCoordinateSpanMake(0.01, 0.01);// MKCoordinateRegion region=MKCoordinateRegionMake(userLocation.location.coordinate, span);// [_mapView setRegion:region animated:true]; }@end
运行效果:
设置大头针视图
在一些应用中系统默认的大头针样式可能无法满足实际的需求,此时就需要修改大头针视图默认样式。根据前面MapKit的代理方法不难发现- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation;方法可以返回一个大头针视图,只要实现这个方法并在这个方法中定义一个大头针视图MKAnnotationView对象并设置相关属性就可以改变默认大头针的样式。MKAnnotationView常用属性:
属性 | 说明 |
annotation | 大头针模型信息,包括标题、子标题、地理位置。 |
image | 大头针图片 |
canShowCallout | 点击大头针是否显示标题、子标题内容等,注意如果在- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation;方法中重新定义大头针默认情况是无法交互的需要设置为true。 |
calloutOffset | 点击大头针时弹出详情信息视图的偏移量 |
selected | 是否被选中状态 |
leftCalloutAccessoryView | 弹出详情左侧视图 |
rightCalloutAccessoryView | 弹出详情右侧视图 |
需要注意:
a.这个代理方法的调用时机:每当有大头针显示到系统可视界面中时就会调用此方法返回一个大头针视图放到界面中,同时当前系统位置标注(也就是地图中蓝色的位置点)也是一个大头针,也会调用此方法,因此处理大头针视图时需要区别对待。
b.类似于UITableView的代理方法,此方法调用频繁,开发过程中需要重复利用MapKit的缓存池将大头针视图缓存起来重复利用。
c.自定义大头针默认情况下不允许交互,如果交互需要设置canShowCallout=true
d.如果代理方法返回nil则会使用默认大头针视图,需要根据情况设置。
下面以一个示例进行大头针视图设置,这里设置了大头针的图片、弹出视图、偏移量等信息。
KCAnnotation.h
// // KCAnnotation.h // MapKit //#import <Foundation/Foundation.h> #import <MapKit/MapKit.h>@interface KCAnnotation : NSObject<MKAnnotation>@property (nonatomic) CLLocationCoordinate2D coordinate; @property (nonatomic, copy) NSString *title; @property (nonatomic, copy) NSString *subtitle;#pragma mark 自定义一个图片属性在创建大头针视图时使用 @property (nonatomic,strong) UIImage *image;@end
KCMainViewController.m
// // KCMainViewController.m // MapKit Annotation // 37.785834 -122.406417 // 39.92 116.39#import "KCMainViewController.h" #import <CoreLocation/CoreLocation.h> #import <MapKit/MapKit.h> #import "KCAnnotation.h"@interface KCMainViewController ()<MKMapViewDelegate>{CLLocationManager *_locationManager;MKMapView *_mapView; }@end@implementation KCMainViewController- (void)viewDidLoad {[super viewDidLoad];[self initGUI]; }#pragma mark 添加地图控件 -(void)initGUI{CGRect rect=[UIScreen mainScreen].bounds;_mapView=[[MKMapView alloc]initWithFrame:rect];[self.view addSubview:_mapView];//设置代理_mapView.delegate=self;//请求定位服务_locationManager=[[CLLocationManager alloc]init];if(![CLLocationManager locationServicesEnabled]||[CLLocationManager authorizationStatus]!=kCLAuthorizationStatusAuthorizedWhenInUse){[_locationManager requestWhenInUseAuthorization];}//用户位置追踪(用户位置追踪用于标记用户当前位置,此时会调用定位服务)_mapView.userTrackingMode=MKUserTrackingModeFollow;//设置地图类型_mapView.mapType=MKMapTypeStandard;//添加大头针[self addAnnotation]; }#pragma mark 添加大头针 -(void)addAnnotation{CLLocationCoordinate2D location1=CLLocationCoordinate2DMake(39.95, 116.35);KCAnnotation *annotation1=[[KCAnnotation alloc]init];annotation1.title=@"CMJ Studio";annotation1.subtitle=@"Kenshin Cui's Studios";annotation1.coordinate=location1;annotation1.image=[UIImage imageNamed:@"icon_pin_floating.png"];[_mapView addAnnotation:annotation1];CLLocationCoordinate2D location2=CLLocationCoordinate2DMake(39.87, 116.35);KCAnnotation *annotation2=[[KCAnnotation alloc]init];annotation2.title=@"Kenshin&Kaoru";annotation2.subtitle=@"Kenshin Cui's Home";annotation2.coordinate=location2;annotation2.image=[UIImage imageNamed:@"icon_paopao_waterdrop_streetscape.png"];[_mapView addAnnotation:annotation2]; }#pragma mark - 地图控件代理方法 #pragma mark 显示大头针时调用,注意方法中的annotation参数是即将显示的大头针对象 -(MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation{//由于当前位置的标注也是一个大头针,所以此时需要判断,此代理方法返回nil使用默认大头针视图if ([annotation isKindOfClass:[KCAnnotation class]]) {static NSString *key1=@"AnnotationKey1";MKAnnotationView *annotationView=[_mapView dequeueReusableAnnotationViewWithIdentifier:key1];//如果缓存池中不存在则新建if (!annotationView) {annotationView=[[MKAnnotationView alloc]initWithAnnotation:annotation reuseIdentifier:key1];annotationView.canShowCallout=true;//允许交互点击annotationView.calloutOffset=CGPointMake(0, 1);//定义详情视图偏移量annotationView.leftCalloutAccessoryView=[[UIImageView alloc]initWithImage:[UIImage imageNamed:@"icon_classify_cafe.png"]];//定义详情左侧视图}//修改大头针视图//重新设置此类大头针视图的大头针模型(因为有可能是从缓存池中取出来的,位置是放到缓存池时的位置)annotationView.annotation=annotation;annotationView.image=((KCAnnotation *)annotation).image;//设置大头针视图的图片return annotationView;}else {return nil;} } @end
运行效果:
注意:
在MapKit框架中除了MKAnnotationView之外还有一个MKPinAnnotationView,它是MKAnnotationView的子类,相比MKAnnotationView多了两个属性pinColor和animationDrop,分别用于设置大头针视图颜色和添加大头针动画。
扩展--自定义大头针弹详情视图
通过上面的示例不难看出MKAnnotationView足够强大(何况还有MKPinAnnotationView),很多信息都可以进行设置,但是唯独不能修改大头针描述详情视图(仅仅支持详情中左右视图内容)。要实现这个需求目前开发中普遍采用的思路就是:
a.点击一个大头针A时重新在A的坐标处添加另一个大头针B(注意此时将A对应的大头针视图canShowCallout设置为false)作为大头针详情模型,然后在- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation;代理方法中判断大头针类型,如果是B则重写MKAnnotationView(可以自定义一个类C继承于MKAnnotationView),返回自定义大头针视图C。
b.定义大头针视图C继承于MKAnnotationView(或者MKPinAnnotationView),在自定义大头针视图中添加自己的控件,完成自定义布局。
在使用百度地图客户端时当点击一个搜索位置时可以看到此位置的评价等信息,视图效果大概如下:
下面不妨试着实现一下这个效果:
大头针模型:KCAnnotation.h
// // KCAnnotation.h // MapKit //#import <Foundation/Foundation.h> #import <MapKit/MapKit.h>@interface KCAnnotation : NSObject<MKAnnotation>@property (nonatomic) CLLocationCoordinate2D coordinate; @property (nonatomic, copy) NSString *title; @property (nonatomic, copy) NSString *subtitle;#pragma mark 自定义一个图片属性在创建大头针视图时使用 @property (nonatomic,strong) UIImage *image;#pragma mark 大头针详情左侧图标 @property (nonatomic,strong) UIImage *icon; #pragma mark 大头针详情描述 @property (nonatomic,copy) NSString *detail; #pragma mark 大头针右下方星级评价 @property (nonatomic,strong) UIImage *rate;@end
弹出详情大头针模型:KCCalloutAnnotation.h
// // KCCalloutAnnotation.h // MapKit //#import <UIKit/UIKit.h> #import <CoreLocation/CoreLocation.h> #import <MapKit/MapKit.h>@interface KCCalloutAnnotation : NSObject<MKAnnotation>@property (nonatomic) CLLocationCoordinate2D coordinate; @property (nonatomic, copy,readonly) NSString *title; @property (nonatomic, copy,readonly) NSString *subtitle;#pragma mark 左侧图标 @property (nonatomic,strong) UIImage *icon; #pragma mark 详情描述 @property (nonatomic,copy) NSString *detail; #pragma mark 星级评价 @property (nonatomic,strong) UIImage *rate;@end
弹出详情大头针视图:KCCalloutAnnotatonView.h
// // KCCalloutView.h // MapKit // 自定义弹出标注视图#import <UIKit/UIKit.h> #import <CoreLocation/CoreLocation.h> #import <MapKit/MapKit.h> #import "KCCalloutAnnotation.h"@interface KCCalloutAnnotationView : MKAnnotationView@property (nonatomic ,strong) KCCalloutAnnotation *annotation;#pragma mark 从缓存取出标注视图 +(instancetype)calloutViewWithMapView:(MKMapView *)mapView;@end
KCCalloutAnnotationView.m
// // KCCalloutView.m // MapKit //#import "KCCalloutAnnotationView.h" #define kSpacing 5 #define kDetailFontSize 12 #define kViewOffset 80@interface KCCalloutAnnotationView(){UIView *_backgroundView;UIImageView *_iconView;UILabel *_detailLabel;UIImageView *_rateView; }@end@implementation KCCalloutAnnotationView-(instancetype)init{if(self=[super init]){[self layoutUI];}return self; } -(instancetype)initWithFrame:(CGRect)frame{if (self=[super initWithFrame:frame]) {[self layoutUI];}return self; }-(void)layoutUI{//背景_backgroundView=[[UIView alloc]init];_backgroundView.backgroundColor=[UIColor whiteColor];//左侧添加图标_iconView=[[UIImageView alloc]init];//上方详情_detailLabel=[[UILabel alloc]init];_detailLabel.lineBreakMode=NSLineBreakByWordWrapping;//[_text sizeToFit];_detailLabel.font=[UIFont systemFontOfSize:kDetailFontSize];//下方星级_rateView=[[UIImageView alloc]init];[self addSubview:_backgroundView];[self addSubview:_iconView];[self addSubview:_detailLabel];[self addSubview:_rateView]; }+(instancetype)calloutViewWithMapView:(MKMapView *)mapView{static NSString *calloutKey=@"calloutKey1";KCCalloutAnnotationView *calloutView=(KCCalloutAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:calloutKey];if (!calloutView) {calloutView=[[KCCalloutAnnotationView alloc]init];}return calloutView; }#pragma mark 当给大头针视图设置大头针模型时可以在此处根据模型设置视图内容 -(void)setAnnotation:(KCCalloutAnnotation *)annotation{[super setAnnotation:annotation];//根据模型调整布局_iconView.image=annotation.icon;_iconView.frame=CGRectMake(kSpacing, kSpacing, annotation.icon.size.width, annotation.icon.size.height);_detailLabel.text=annotation.detail;float detailWidth=150.0;CGSize detailSize= [annotation.detail boundingRectWithSize:CGSizeMake(detailWidth, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:kDetailFontSize]} context:nil].size;float detailX=CGRectGetMaxX(_iconView.frame)+kSpacing;_detailLabel.frame=CGRectMake(detailX, kSpacing, detailSize.width, detailSize.height);_rateView.image=annotation.rate;_rateView.frame=CGRectMake(detailX, CGRectGetMaxY(_detailLabel.frame)+kSpacing, annotation.rate.size.width, annotation.rate.size.height);float backgroundWidth=CGRectGetMaxX(_detailLabel.frame)+kSpacing;float backgroundHeight=_iconView.frame.size.height+2*kSpacing;_backgroundView.frame=CGRectMake(0, 0, backgroundWidth, backgroundHeight);self.bounds=CGRectMake(0, 0, backgroundWidth, backgroundHeight+kViewOffset);} @end
主视图控制器:KCMainViewController.m
// // KCMainViewController.m // MapKit Annotation // 37.785834 -122.406417 // 39.92 116.39#import "KCMainViewController.h" #import <CoreLocation/CoreLocation.h> #import <MapKit/MapKit.h> #import "KCAnnotation.h" #import "KCCalloutAnnotationView.h" #import "KCCalloutAnnotationView.h"@interface KCMainViewController ()<MKMapViewDelegate>{CLLocationManager *_locationManager;MKMapView *_mapView; }@end@implementation KCMainViewController- (void)viewDidLoad {[super viewDidLoad];[self initGUI]; }#pragma mark 添加地图控件 -(void)initGUI{CGRect rect=[UIScreen mainScreen].bounds;_mapView=[[MKMapView alloc]initWithFrame:rect];[self.view addSubview:_mapView];//设置代理_mapView.delegate=self;//请求定位服务_locationManager=[[CLLocationManager alloc]init];if(![CLLocationManager locationServicesEnabled]||[CLLocationManager authorizationStatus]!=kCLAuthorizationStatusAuthorizedWhenInUse){[_locationManager requestWhenInUseAuthorization];}//用户位置追踪(用户位置追踪用于标记用户当前位置,此时会调用定位服务)_mapView.userTrackingMode=MKUserTrackingModeFollow;//设置地图类型_mapView.mapType=MKMapTypeStandard;//添加大头针[self addAnnotation]; }#pragma mark 添加大头针 -(void)addAnnotation{CLLocationCoordinate2D location1=CLLocationCoordinate2DMake(39.95, 116.35);KCAnnotation *annotation1=[[KCAnnotation alloc]init];annotation1.title=@"CMJ Studio";annotation1.subtitle=@"Kenshin Cui's Studios";annotation1.coordinate=location1;annotation1.image=[UIImage imageNamed:@"icon_pin_floating.png"];annotation1.icon=[UIImage imageNamed:@"icon_mark1.png"];annotation1.detail=@"CMJ Studio...";annotation1.rate=[UIImage imageNamed:@"icon_Movie_Star_rating.png"];[_mapView addAnnotation:annotation1];CLLocationCoordinate2D location2=CLLocationCoordinate2DMake(39.87, 116.35);KCAnnotation *annotation2=[[KCAnnotation alloc]init];annotation2.title=@"Kenshin&Kaoru";annotation2.subtitle=@"Kenshin Cui's Home";annotation2.coordinate=location2;annotation2.image=[UIImage imageNamed:@"icon_paopao_waterdrop_streetscape.png"];annotation2.icon=[UIImage imageNamed:@"icon_mark2.png"];annotation2.detail=@"Kenshin Cui...";annotation2.rate=[UIImage imageNamed:@"icon_Movie_Star_rating.png"];[_mapView addAnnotation:annotation2]; }#pragma mark - 地图控件代理方法 #pragma mark 显示大头针时调用,注意方法中的annotation参数是即将显示的大头针对象 -(MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation{//由于当前位置的标注也是一个大头针,所以此时需要判断,此代理方法返回nil使用默认大头针视图if ([annotation isKindOfClass:[KCAnnotation class]]) {static NSString *key1=@"AnnotationKey1";MKAnnotationView *annotationView=[_mapView dequeueReusableAnnotationViewWithIdentifier:key1];//如果缓存池中不存在则新建if (!annotationView) {annotationView=[[MKAnnotationView alloc]initWithAnnotation:annotation reuseIdentifier:key1]; // annotationView.canShowCallout=true;//允许交互点击annotationView.calloutOffset=CGPointMake(0, 1);//定义详情视图偏移量annotationView.leftCalloutAccessoryView=[[UIImageView alloc]initWithImage:[UIImage imageNamed:@"icon_classify_cafe.png"]];//定义详情左侧视图}//修改大头针视图//重新设置此类大头针视图的大头针模型(因为有可能是从缓存池中取出来的,位置是放到缓存池时的位置)annotationView.annotation=annotation;annotationView.image=((KCAnnotation *)annotation).image;//设置大头针视图的图片return annotationView;}else if([annotation isKindOfClass:[KCCalloutAnnotation class]]){//对于作为弹出详情视图的自定义大头针视图无弹出交互功能(canShowCallout=false,这是默认值),在其中可以自由添加其他视图(因为它本身继承于UIView)KCCalloutAnnotationView *calloutView=[KCCalloutAnnotationView calloutViewWithMapView:mapView];calloutView.annotation=annotation;return calloutView;} else {return nil;} }#pragma mark 选中大头针时触发 //点击一般的大头针KCAnnotation时添加一个大头针作为所点大头针的弹出详情视图 -(void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view{KCAnnotation *annotation=view.annotation;if ([view.annotation isKindOfClass:[KCAnnotation class]]) {//点击一个大头针时移除其他弹出详情视图 // [self removeCustomAnnotation];//添加详情大头针,渲染此大头针视图时将此模型对象赋值给自定义大头针视图完成自动布局KCCalloutAnnotation *annotation1=[[KCCalloutAnnotation alloc]init];annotation1.icon=annotation.icon;annotation1.detail=annotation.detail;annotation1.rate=annotation.rate;annotation1.coordinate=view.annotation.coordinate;[mapView addAnnotation:annotation1];} }#pragma mark 取消选中时触发 -(void)mapView:(MKMapView *)mapView didDeselectAnnotationView:(MKAnnotationView *)view{[self removeCustomAnnotation]; }#pragma mark 移除所用自定义大头针 -(void)removeCustomAnnotation{[_mapView.annotations enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {if ([obj isKindOfClass:[KCCalloutAnnotation class]]) {[_mapView removeAnnotation:obj];}}]; } @end
在这个过程中需要注意几点:
1.大头针A作为一个普通大头针,其中最好保存自定义大头针视图C所需要的模型以便根据不同的模型初始化视图。
2.自定义大头针视图C的大头针模型B中不需要title、subtitle属性,最好设置为只读;模型中最后保存自定义大头针视图C所需要的布局模型数据。
3.只有点击非B类大头针时才新增自定义大头针,并且增加时要首先移除其他B类大头针避免重叠(一般建议放到取消大头针选择的代理方法中)。
4.通常在自定义大头针视图C设置大头针模型时布局界面,此时需要注意新增大头针的位置,通常需要偏移一定的距离才能达到理想的效果。
运行效果:
使用自带的地图应用
除了可以使用MapKit框架进行地图开发,对地图有精确的控制和自定义之外,如果对于应用没有特殊要求的话选用苹果自带的地图应用也是一个不错的选择。使用苹果自带的应用时需要用到MapKit中的MKMapItem类,这个类有一个openInMapsWithLaunchOptions:动态方法和一个openMapsWithItems: launchOptions:静态方法用于打开苹果地图应用。第一个方法用于在地图上标注一个位置,第二个方法除了可以标注多个位置外还可以进行多个位置之间的驾驶导航,使用起来也是相当方便。在熟悉这两个方法使用之前有必要对两个方法中的options参数做一下简单说明:
键(常量) | 说明 | 值 |
MKLaunchOptionsDirectionsModeKey | 路线模式,常量 | MKLaunchOptionsDirectionsModeDriving 驾车模式 MKLaunchOptionsDirectionsModeWalking 步行模式 |
MKLaunchOptionsMapTypeKey | 地图类型,枚举 | MKMapTypeStandard :标准模式 MKMapTypeSatellite :卫星模式 MKMapTypeHybrid :混合模式 |
MKLaunchOptionsMapCenterKey | 中心点坐标,CLLocationCoordinate2D类型 | |
MKLaunchOptionsMapSpanKey | 地图显示跨度,MKCoordinateSpan 类型 | |
MKLaunchOptionsShowsTrafficKey | 是否 显示交通状况,布尔型 | |
MKLaunchOptionsCameraKey | 3D地图效果,MKMapCamera类型 注意:此属性从iOS7及以后可用,前面的属性从iOS6开始可用 |
单个位置的标注
下面的代码演示了如何在苹果自带地图应用上标记一个位置,首先根据反地理编码获得一个CLPlacemark位置对象,然后将其转换为MKPlacemark对象用于MKMapItem初始化,最后调用其openInMapsWithLaunchOptions:打开地图应用并标记:
// // KCMainViewController.m // AppleMap //#import "KCMainViewController.h" #import <CoreLocation/CoreLocation.h> #import <MapKit/MapKit.h>@interface KCMainViewController () @property (nonatomic,strong) CLGeocoder *geocoder; @end@implementation KCMainViewController- (void)viewDidLoad {[super viewDidLoad];_geocoder=[[CLGeocoder alloc]init];[self location]; }#pragma mark 在地图上定位 -(void)location{//根据“北京市”进行地理编码[_geocoder geocodeAddressString:@"北京市" completionHandler:^(NSArray *placemarks, NSError *error) {CLPlacemark *clPlacemark=[placemarks firstObject];//获取第一个地标MKPlacemark *mkplacemark=[[MKPlacemark alloc]initWithPlacemark:clPlacemark];//定位地标转化为地图的地标NSDictionary *options=@{MKLaunchOptionsMapTypeKey:@(MKMapTypeStandard)};MKMapItem *mapItem=[[MKMapItem alloc]initWithPlacemark:mkplacemark];[mapItem openInMapsWithLaunchOptions:options];}]; } @end
运行效果:
标记多个位置
如果要标记多个位置需要调用MKMapItem的静态方法,下面的代码演示中需要注意,使用CLGeocoder进行定位时一次只能定位到一个位置,所以第二个位置定位放到了第一个位置获取成功之后。
// // KCMainViewController.m // AppleMap#import "KCMainViewController.h" #import <CoreLocation/CoreLocation.h> #import <MapKit/MapKit.h>@interface KCMainViewController () @property (nonatomic,strong) CLGeocoder *geocoder; @end@implementation KCMainViewController- (void)viewDidLoad {[super viewDidLoad];_geocoder=[[CLGeocoder alloc]init];[self listPlacemark]; }-(void)listPlacemark{//根据“北京市”进行地理编码[_geocoder geocodeAddressString:@"北京市" completionHandler:^(NSArray *placemarks, NSError *error) {CLPlacemark *clPlacemark1=[placemarks firstObject];//获取第一个地标MKPlacemark *mkPlacemark1=[[MKPlacemark alloc]initWithPlacemark:clPlacemark1];//注意地理编码一次只能定位到一个位置,不能同时定位,所在放到第一个位置定位完成回调函数中再次定位[_geocoder geocodeAddressString:@"郑州市" completionHandler:^(NSArray *placemarks, NSError *error) {CLPlacemark *clPlacemark2=[placemarks firstObject];//获取第一个地标MKPlacemark *mkPlacemark2=[[MKPlacemark alloc]initWithPlacemark:clPlacemark2];NSDictionary *options=@{MKLaunchOptionsMapTypeKey:@(MKMapTypeStandard)};//MKMapItem *mapItem1=[MKMapItem mapItemForCurrentLocation];//当前位置MKMapItem *mapItem1=[[MKMapItem alloc]initWithPlacemark:mkPlacemark1];MKMapItem *mapItem2=[[MKMapItem alloc]initWithPlacemark:mkPlacemark2];[MKMapItem openMapsWithItems:@[mapItem1,mapItem2] launchOptions:options];}];}]; } @end
运行效果:
地图导航
要使用地图导航功能在自带地图应用中相当简单,只要设置参数配置导航模式即可,例如在上面代码基础上设置驾驶模式,则地图应用会启动驾驶模式计算两点之间的距离同时对路线进行规划。
// // KCMainViewController.m // AppleMap //#import "KCMainViewController.h" #import <CoreLocation/CoreLocation.h> #import <MapKit/MapKit.h>@interface KCMainViewController () @property (nonatomic,strong) CLGeocoder *geocoder; @end@implementation KCMainViewController- (void)viewDidLoad {[super viewDidLoad];_geocoder=[[CLGeocoder alloc]init];[self turnByTurn]; }-(void)turnByTurn{//根据“北京市”地理编码[_geocoder geocodeAddressString:@"北京市" completionHandler:^(NSArray *placemarks, NSError *error) {CLPlacemark *clPlacemark1=[placemarks firstObject];//获取第一个地标MKPlacemark *mkPlacemark1=[[MKPlacemark alloc]initWithPlacemark:clPlacemark1];//注意地理编码一次只能定位到一个位置,不能同时定位,所在放到第一个位置定位完成回调函数中再次定位[_geocoder geocodeAddressString:@"郑州市" completionHandler:^(NSArray *placemarks, NSError *error) {CLPlacemark *clPlacemark2=[placemarks firstObject];//获取第一个地标MKPlacemark *mkPlacemark2=[[MKPlacemark alloc]initWithPlacemark:clPlacemark2];NSDictionary *options=@{MKLaunchOptionsMapTypeKey:@(MKMapTypeStandard),MKLaunchOptionsDirectionsModeKey:MKLaunchOptionsDirectionsModeDriving};//MKMapItem *mapItem1=[MKMapItem mapItemForCurrentLocation];//当前位置MKMapItem *mapItem1=[[MKMapItem alloc]initWithPlacemark:mkPlacemark1];MKMapItem *mapItem2=[[MKMapItem alloc]initWithPlacemark:mkPlacemark2];[MKMapItem openMapsWithItems:@[mapItem1,mapItem2] launchOptions:options];}];}]; } @end
运行效果:
注意:其实如果不用苹果自带的地图应用也可以实现地图导航,MapKit中提供了MKDirectionRequest对象用于计算路线,提供了MKDirections用于计算方向,这样一来只需要调用MKMapView的addOverlay等方法添加覆盖物即可实现类似的效果,有兴趣的朋友可以试一下。
由于定位和地图框架中用到了诸多类,有些初学者容易混淆,下面简单对比一下。
CLLocation:用于表示位置信息,包含地理坐标、海拔等信息,包含在CoreLoaction框架中。
MKUserLocation:一个特殊的大头针,表示用户当前位置。
CLPlacemark:定位框架中地标类,封装了详细的地理信息。
MKPlacemark:类似于CLPlacemark,只是它在MapKit框架中,可以根据CLPlacemark创建MKPlacemark。