【原】iOS学习48地图
一、地图的简介
在移动互联网时代,移动app能解决用户的很多生活琐事,比如
导航:去任意陌生的地方
周边:找餐馆、找酒店、找银行、找电影院
手机软件:微信摇一摇、QQ附近的人、微博、支付宝等
在上述应用中,都用到了地图和定位功能,在iOS开发中,要想加入这两大功能,必须基于两个框架进行开发
MapKit :用于地图展示
CoreLocation :用于地理定位
二、地图定位(CoreLocation框架,地理编码与反地理编码)
1、CoreLocation框架的使用
- 导入框架 (iOS5之后不再需要)
- 导入头文件
#import <CoreLocation/CoreLocation.h>
- CoreLocation框架使用须知
CoreLocation 框架中所有数据类型的前缀都是CL
CoreLocation 中使用 CLLocationManager 对象来做用户定位
2、CLLocationManager的常用操作(详细使用说明见注释)
// 开始用户定位 - (void)startUpdatingLocation; // 停止用户定位 - (void) stopUpdatingLocation; // 每隔多少米定位一次 @property(assign, nonatomic) CLLocationDistance distanceFilter; // 定位精确度(越精确就越耗电) @property(assign, nonatomic) CLLocationAccuracy desiredAccuracy; // 当调用了startUpdatingLocation方法后,就开始不断地定位用户的位置,中途会频繁地调用下面的代理方法 - (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations; // 参数locations中的元素对象是CLLocation对象,是一系列的位置信息 // 当定位失败的时候,会调用下面的代理方法 - (void)locationManager:(CLLocationManager *)manager monitoringDidFailForRegion:(nullable CLRegion *)region withError:(NSError *)error;
CLLocationAccuracy 是一个枚举值
/* 最佳导航 kCLLocationAccuracyBestForNavigation 最精准 kCLLocationAccuracyBest; 10米 kCLLocationAccuracyNearestTenMeters; 百米 kCLLocationAccuracyHundredMeters; 千米 kCLLocationAccuracyKilometer; 3千米 kCLLocationAccuracyThreeKilometers; */
3、CLLocation
CLLocation用来表示某个位置的地理信息,比如经纬度、海拔等等
// 经纬度 @property(readonly, nonatomic) CLLocationCoordinate2D coordinate; // 海拔 @property(readonly, nonatomic) CLLocationDistance altitude; // 路线,航向(取值范围是0.0° ~ 359.9°,0.0°代表正北方向) @property(readonly, nonatomic) CLLocationDirection course; // 行走速度(单位是m/s) @property(readonly, nonatomic) CLLocationSpeed speed; // 此方法可以计算2个位置(CLLocation)之间的距离 - (CLLocationDistance)distanceFromLocation:(const CLLocation *)location
4、CLLocationCoordinate2D
CLLocationCoordinate2D 是一个用来表示经纬度的结构体,定义如下
typedef struct { CLLocationDegrees latitude; // 纬度 CLLocationDegrees longitude; // 经度 } CLLocationCoordinate2D;
一般用 CLLocationCoordinate2DMake 函数来创建 CLLocationCoordinate2D
5、实现定位的步骤
- 第一步:初始化定位管理器
self.manager = [[CLLocationManager alloc] init];
- 第二步:进行隐私的判断和授权
1> 进行定位服务是否开启的判断
若 locationServicesEnabled 为 NO,就跳转到隐私界面进行设置
2> 进行版本判断(iOS8.0前后的定位方式不一样)获取版本数:[[[UIDevice currentDevice] systemVersion] integerValue]
3> iOS8.0之后授权的操作
① 判断授权状态:authorizationStatus
五种状态枚举值
kCLAuthorizationStatusNotDetermined: 用户尚未做出决定是否启用定位服务
kCLAuthorizationStatusRestricted: 没有获得用户授权使用定位服务,可能用户没有自己禁止访问授权
kCLAuthorizationStatusDenied:用户已经明确禁止应用使用定位服务或者当前系统定位服务处于关闭状态
kCLAuthorizationStatusAuthorizedAlways: 应用获得授权可以一直使用定位服务,即使应用不在使用状态(常用)
kCLAuthorizationStatusAuthorizedWhenInUse: 使用此应用过程中允许访问定位服务(常用)
② 在授权请求之前需要在infoPlist文件中设置 NSLocationUsageDescription 允许定位内容
authorizationStatus 为 kCLAuthorizationStatusAuthorizedWhenInUse 时使用NSLocationWhenInUseUsageDescription 进行设置
authorizationStatus 为 kCLAuthorizationStatusAuthorizedAlways 时使用 NSLocationAlwaysUsageDescription 进行设置
一旦用户选择了 "Don’t Allow",意味着你的应用以后就无法使用定位功能为了严谨起见,最好在使用定位功能之前判断当前应用的定位功能是否可用,也就是我的 1> 进行定位服务是否开启的判断
③ 请求授权
requestWhenInUseAuthorization要和infoPlist文件中的匹配(NSLocationWhenInUseUsageDescription)
requestAlwaysAuthorization要和infoPlist文件中的匹配(NSLocationAlwaysUsageDescription)
// 第二步:进行隐私的判断和授权 // 进行隐私的判断 if (![CLLocationManager locationServicesEnabled]) { NSLog(@"是否前往隐私进行设置允许定位"); } // 进行版本的判断 if ([[[UIDevice currentDevice] systemVersion] integerValue] >= 8.0) { // 根据状态进行授权 if ([CLLocationManager authorizationStatus] != kCLAuthorizationStatusAuthorizedWhenInUse) { // 请求授权 [self.manager requestWhenInUseAuthorization];
[self.manager requestWhenInUseAuthorization]; }
}
注意:从iOS 7之后,苹果在保护用户隐私方面做了很大的加强,以下操作都必须经过用户批准授权:
① 要想获得用户的位置和访问用户的通讯录、日历、相机、相册等等都需要用户来手动授权。
② 当想访问用户的隐私信息时,系统会自动弹出一个对话框让用户授权
- 第三步:设置管理器的代理和相关属性
// 第三步:设置管理器的代理和相关属性 self.manager.delegate = self; // 设置精度 self.manager.desiredAccuracy = kCLLocationAccuracyHundredMeters; // 设置最小更新距离 self.manager.distanceFilter = 10;
CLLocationManagerDelegate
定位成功后开始更新位置信息,只要移动的距离大于设置最小更新距离时也开始走这个方法:
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations { // 获取最后一次的位置 CLLocation *location = locations.lastObject; // 获取位置坐标 CLLocationCoordinate2D coordinate = location.coordinate; // 打印经纬度,longitude 经度 latitude 纬度 NSLog(@"经度:%f, 纬度:%f, 海拔:%f, 航向:%f, 行走速度:%f", coordinate.longitude, coordinate.latitude, location.altitude, location.course, location.speed); // 为了节省电源,如果不使用定位,需要把定位关闭 [self.manager stopUpdatingLocation]; }
定位失败
- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error { NSLog(@"定位失败"); }
6、地理编码与地理反编码
- 使用CLGeocoder可以完成"地理编码"和"反地理编码"
地理编码:根据给定的地名,获得具体的位置信息(比如经纬度、地址的全称等)
反地理编码:根据给定的经纬度,获得具体的位置信息
- 具体方法:
// 地理编码方法 - (void)geocodeAddressString:(NSString *)addressString completionHandler:(CLGeocodeCompletionHandler)completionHandler; // 反地理编码方法 - (void)reverseGeocodeLocation:(CLLocation *)location completionHandler:(CLGeocodeCompletionHandler)completionHandler;
- CLGeocodeCompletionHandler
当地理编码/反地理编码完成时,就会调用 CLGeocodeCompletionHandler,头文件声明:
typedef void (^CLGeocodeCompletionHandler)(NSArray<CLPlacemark *> *placemarks, NSError *error);
block包含2个参数
error:当编码出错时(比如编码不出具体的信息),2其错误信息会包含在error中
placemarks:里面装着 CLPlacemark 对象
- CLPlacemark
CLPlacemark的字面意思是地标,封装详细的地址位置信息,相关属性:
// 地理位置 @property (nonatomic, readonly) CLLocation *location; // 区域 @property (nonatomic, readonly) CLRegion *region; // 详细的地址信息 @property (nonatomic, readonly) NSDictionary *addressDictionary; // 地址名称 @property (nonatomic, readonly) NSString *name; // 地点名称 @property (nonatomic, readonly) NSString *locality;
7、详细实例代码
#import "ViewController.h" // 第一步:引入库的头文件 #import <CoreLocation/CoreLocation.h> @interface ViewController () <CLLocationManagerDelegate> // CoreLocation框架中的CLLocationManager用于管理定位的管理器 /// 定位管理器 @property (nonatomic, strong) CLLocationManager *manager; /// 编码和反编码类 @property (nonatomic, strong) CLGeocoder *geocoder; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. // 定位的步骤 // 第一步:初始化定位管理器 self.manager = [[CLLocationManager alloc] init]; // 第二步:进行隐私的判断和授权 // 进行隐私的判断 if (![CLLocationManager locationServicesEnabled]) { NSLog(@"是否前往隐私进行设置允许定位"); } // 进行版本的判断 if ([[[UIDevice currentDevice] systemVersion] integerValue] >= 8.0) { // 根据状态进行授权 if ([CLLocationManager authorizationStatus] != kCLAuthorizationStatusAuthorizedWhenInUse) { // 请求授权 [self.manager requestWhenInUseAuthorization]; } } // 第三步:设置管理器的代理和相关属性 self.manager.delegate = self; // 设置精度 self.manager.desiredAccuracy = kCLLocationAccuracyHundredMeters; // 设置最小更新距离 self.manager.distanceFilter = 10; // 第四步:开启定位 [self.manager startUpdatingLocation]; /***********************编码和反编码**********************/ // 初始化对象 self.geocoder = [[CLGeocoder alloc] init]; // 根据地名获取经纬度 // [self getCoordinateByAddress:@"中国山西省朔州市右玉县新城镇绿源小区"]; // 编译和反编译不能同时走,设置不同的按钮来分别实现 // 根据经纬度反编译取出地名 // [self getAddressByLongitude:112.4750 Latitude:39.98]; // 计算两点之间的距离 // [self distanceForDeuce]; } #pragma mark - 计算两点之间的距离 - (void)distanceForDeuce { // 创建位置 CLLocation *locationBeijing = [[CLLocation alloc] initWithLatitude:40 longitude:116]; CLLocation *locationYouyu = [[CLLocation alloc] initWithLatitude:39.98 longitude:112.47]; // 计算距离 CLLocationDistance distance = [locationBeijing distanceFromLocation:locationYouyu]; NSLog(@"%f公里", distance / 1000); } #pragma mark - 根据经纬度反编译取出地名 - (void)getAddressByLongitude:(CLLocationDegrees)longitude Latitude:(CLLocationDegrees)latitude { // 反编码 // 创建CLLocation CLLocation *location = [[CLLocation alloc] initWithLatitude:latitude longitude:longitude]; [self.geocoder reverseGeocodeLocation:location completionHandler:^(NSArray<CLPlacemark *> * _Nullable placemarks, NSError * _Nullable error) { NSDictionary *dict = placemarks.firstObject.addressDictionary; NSLog(@"反编码地理位置信息:%@", dict); }]; } #pragma mark - 根据地名获取经纬度 - (void)getCoordinateByAddress:(NSString *)address { [self.geocoder geocodeAddressString:address completionHandler:^(NSArray<CLPlacemark *> * _Nullable placemarks, NSError * _Nullable error) { // 根据返回的地标,取出起一个位置(地标位置很多) CLPlacemark *mark = placemarks.firstObject; // 根据地标得到location CLLocation *location = mark.location; // 根据地标获取地区 CLRegion *region = mark.region; // 获取字典信息 NSDictionary *addressDict = mark.addressDictionary; NSLog(@"location = %@\n region = %@\n addressDict = %@", location, region, addressDict); // 根据具体的需求进行选择输出 // 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; //关联的或利益相关的地标 }]; } #pragma mark - CLLocationManagerDelegate的代理方法 // 定位成功后开始更新位置信息,只要移动的距离大于设置最小更新距离时也开始走这个方法 - (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations { // 获取最后一次的位置 CLLocation *location = locations.lastObject; // 获取位置坐标 CLLocationCoordinate2D coordinate = location.coordinate; // 打印经纬度,longitude 经度 latitude 纬度 NSLog(@"经度:%f, 纬度:%f, 海拔:%f, 航向:%f, 行走速度:%f", coordinate.longitude, coordinate.latitude, location.altitude, location.course, location.speed); // 为了节省电源,如果不使用定位,需要把定位关闭 [self.manager stopUpdatingLocation]; } - (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error { NSLog(@"定位失败"); } @end
三、地图显示(MapKit框架)
1、MapKit框架的使用
- 导入框架(iOS5之后不再需要程序员自己导入)
- 导入主头文件
#import <MapKit/MapKit.h>
- MapKit框架使用须知
MapKit框架中所有数据类型的前缀都是MK
MapKit有一个比较重要的UI控件:MKMapView,专门用于地图显示
2、跟踪显示用户的位置
设置MKMapView的userTrackingMode属性可以跟踪显示用户的当前位置
MKUserTrackingModeNone :不跟踪用户的位置
MKUserTrackingModeFollow :跟踪并在地图上显示用户的当前位置
MKUserTrackingModeFollowWithHeading :跟踪并在地图上显示用户的当前位置,地图会跟随用户的前进方向进行旋转
下图是跟踪效果:
蓝色发光圆点就是用户的当前位置,专业术语叫做 "大头针"
3、地图的类型
可以通过设置MKMapView的mapType设置地图类型(mapViewType是枚举类型)
MKMapTypeStandard // 普通地图(左图) MKMapTypeSatellite // 卫星云图 (中图) MKMapTypeHybrid // 普通地图覆盖于卫星云图之上(右图) MKMapTypeSatelliteFlyover NS_ENUM_AVAILABLE(10_11, 9_0) // 地形和建筑物的三维模型 MKMapTypeHybridFlyover NS_ENUM_AVAILABLE(10_11, 9_0) // 显示道路和附加元素的Flyover
4、MKMapView的代理
- MKMapView可以设置一个代理对象 MKMapViewDelegate,用来监听地图的相关行为,常见的代理方法有:
// 一个位置更改默认只会调用一次,不断监测用户的当前位置时每次都会调用这个方法,把用户的最新位置(userLocation参数)传进来. - (void)mapView:(MKMapView *)mapView didUpdateUserLocation:(MKUserLocation *)userLocation; // 地图的显示区域即将发生改变的时候调用 - (void)mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated; // 地图的显示区域已经发生改变的时候调用 - (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated;
- MKUserLocation其实是个大头针模型,包括以下属性:
// 显示在大头针上的标题 @property (nonatomic, copy) NSString *title; // 显示在大头针上的子标题 @property (nonatomic, copy) NSString *subtitle; // 地理位置信息(大头针钉在什么地方) @property (readonly, nonatomic) CLLocation *location;
5、设置地图的显示
- 通过MKMapView的下列方法,可以设置地图显示的位置和区域
// 设置地图的中心点位置 @property (nonatomic) CLLocationCoordinate2D centerCoordinate; -(void)setCenterCoordinate: (CLLocationCoordinate2D)coordinate animated:(BOOL)animated; @property (nonatomic) MKCoordinateRegion region; - (void)setRegion:(MKCoordinateRegion)region animated:(BOOL)animated;
- MKCoordinateRegion是一个用来表示区域的结构体,定义如下:
typedef struct { CLLocationCoordinate2D center; // 区域的中心点位置 MKCoordinateSpan span; // 区域的跨度 } MKCoordinateRegion;
- MKCoordinateSpan的定义:
typedef struct { CLLocationDegrees latitudeDelta; // 纬度跨度 CLLocationDegrees longitudeDelta; // 经度跨度 } MKCoordinateSpan;
6、大头针
- 新建一个大头针模型类,继承 NSObject,遵循 MKAnnotation 协议就是一个大头针!
#import <Foundation/Foundation.h> #import <MapKit/MapKit.h> @interface MyAnnotation : NSObject <MKAnnotation> // 重写协议中的三个属性:coordinate(标记位置)、title(标题)、subtitle(子标题) /// 坐标 @property (nonatomic) CLLocationCoordinate2D coordinate; /// 标题 @property (nonatomic, copy) NSString *title; /// 子标题 @property (nonatomic, copy) NSString *subtitle; @end
- 大头针的基本操作
// 添加一个大头针 - (void)addAnnotation:(id <MKAnnotation>)annotation; // 添加多个大头针 - (void)addAnnotations:(NSArray *)annotations; // 移除一个大头针 - (void)removeAnnotation:(id <MKAnnotation>)annotation; // 移除多个大头针 - (void)removeAnnotations:(NSArray *)annotations;
7、简单实例代码
#import "ViewController.h" // 地图使用的空间 #import <MapKit/MapKit.h> #import <CoreLocation/CoreLocation.h> #import "MyAnnotation.h" @interface ViewController () <MKMapViewDelegate> /// 定位管理器 @property (nonatomic, strong) CLLocationManager *locationManager; /// 显示地图的视图 @property (nonatomic, strong) MKMapView *mapView; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. // 创建地图视图 [self createMapView]; } #pragma mark - 创建地图视图 - (void)createMapView { // 创建地图,并添加到当前视图上 self.mapView = [[MKMapView alloc] initWithFrame:[UIScreen mainScreen].bounds]; [self.view addSubview:self.mapView]; // 设置代理 self.mapView.delegate = self; // 定位 self.locationManager = [[CLLocationManager alloc] init]; if (![CLLocationManager locationServicesEnabled]) { NSLog(@"当前设备定位不可用"); } if ([CLLocationManager authorizationStatus] != kCLAuthorizationStatusAuthorizedWhenInUse) { [self.locationManager requestWhenInUseAuthorization]; } // 设置地图的定位追踪的模式 self.mapView.userTrackingMode = MKUserTrackingModeFollow; // 设置地图的显示类型 self.mapView.mapType = MKMapTypeStandard; // 添加大头针 [self addAnnotation]; } #pragma mark - 添加大头针 - (void)addAnnotation { // 设置位置北京 CLLocationCoordinate2D location1 = CLLocationCoordinate2DMake(40, 116); MyAnnotation *annotation = [[MyAnnotation alloc] init]; annotation.coordinate = location1; annotation.title = @"北京"; annotation.subtitle = @"一个古老的城市"; // 设置位置右玉 CLLocationCoordinate2D location2 = CLLocationCoordinate2DMake(39.98, 112.47); MyAnnotation *annotation2 = [[MyAnnotation alloc] init]; annotation2.coordinate = location2; annotation2.title = @"右玉"; annotation2.subtitle = @"生我养我的地方"; // 大连 CLLocationCoordinate2D location3 = CLLocationCoordinate2DMake(38.92, 121.62); MyAnnotation *annotation3 = [[MyAnnotation alloc] init]; annotation3.coordinate = location3; annotation3.title = @"大连"; annotation3.subtitle = @"一个喝啤酒吃蛤蜊的城市"; NSArray *annotationArray = @[annotation, annotation2, annotation3]; [self.mapView addAnnotations:annotationArray]; } #pragma mark - 代理方法 - (void)mapView:(MKMapView *)mapView didUpdateUserLocation:(MKUserLocation *)userLocation { NSLog(@"%@", userLocation); } @end
四、自定义大头针
很多情况下,需要自定义大头针的显示样式,比如显示一张图片
1、设置MKMapView的代理
实现下面的代理方法,返回大头针控件
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation; // 根据传进来的(id <MKAnnotation>)annotation参数创建并返回对应的大头针控件
代理方法的使用注意
如果返回nil,显示出来的大头针就采取系统的默认样式
标识用户位置的蓝色发光圆点,它也是一个大头针,当显示这个大头针时,也会调用代理方法
因此,需要在代理方法中分清楚(id <MKAnnotation>)annotation参数代表自定义的大头针还是蓝色发光圆点
2、MKAnnotationView
地图上的大头针控件是 MKAnnotationView,MKAnnotationView 的属性:
// 大头针模型 @property (nonatomic, strong) id <MKAnnotation> annotation; // 显示的图片 @property (nonatomic, strong) UIImage *image; // 是否显示标注 @property (nonatomic) BOOL canShowCallout; // 标注的偏移量 @property (nonatomic) CGPoint calloutOffset; // 标注右边显示什么控件 @property (strong, nonatomic) UIView *rightCalloutAccessoryView; // 标注左边显示什么控件 @property (strong, nonatomic) UIView *leftCalloutAccessoryView;
3、MKPinAnnotationView
MKPinAnnotationView 是 MKAnnotationView 的子类
MKPinAnnotationView 比 MKAnnotationView 多了2个属性:
// 大头针颜色 @property (nonatomic) MKPinAnnotationColor pinColor; // 大头针第一次显示时是否从天而降 @property (nonatomic) BOOL animatesDrop;
4、实例代码
#pragma mark - 实现自定义大头针的代理方法 - (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation { // 判断是否是当前自定义大头针类 if ([annotation isKindOfClass:[MyAnnotation class]]) { // 先定义一个重用标识(和cell一样) static NSString *identifiter = @"AnnotationOne"; MKAnnotationView *annotationView = [self.mapView dequeueReusableAnnotationViewWithIdentifier:identifiter]; if (annotationView == nil) { annotationView = [[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:identifiter]; // 允许用户交互 annotationView.canShowCallout = YES; // 设置详情和大头针的头偏移量 annotationView.calloutOffset = CGPointMake(0, 1); // 设置详情的左视图 annotationView.leftCalloutAccessoryView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"1.jpg"]]; } // 修改大头针视图 annotationView.annotation = annotation; // 一定要赋值 annotationView.image = ((MyAnnotation *)annotation).image; return annotationView; } else { return nil; } }