地图系列之一CoreLocation框架

简介

  1. 在移动互联网时代,移动app能解决用户的很多生活琐事,比如
    1. 周边:找餐馆、找KTV、找电影院等等
    2. 导航:根据用户设定的起点和终点,进行路线规划,并指引用户如何到达
  2. 在上述应用中,都用到了定位地图功能,在iOS开发中,要想加入这2大功能,必须基于2个框架进行开发
    1. CoreLocation :用于地理定位,地理编码,区域监听等(着重功能实现)
    2. MapKit :用于地图展示,例如大头针,路线、覆盖层展示等(着重界面展示)
    3. 说明: 这两个框架MapKit相对来说更加重量级,MapKit定位是相对于CoreLocation来做的,因此要先研究CoreLocation
  3. 2个热门专业术语
    1. LBS :Location Based Service(基于定位的服务)
    2. SoLoMo :Social Local Mobile(索罗门)

CoreLocation框架

  1. CoreLocation框架使用前提
    1. 导入框架(Xcode5.0之后可以省略)
    2. 导入主头文件#import <CoreLocation/CoreLocation.h>
  2. CoreLocation框架使用须知
    1. CoreLocation框架中所有数据类型的前缀都是CL
    2. CoreLocation中使用CLLocationManager对象来做用户定位

用户隐私的保护

1. iOS<8.0

  1. 从iOS 6开始,苹果在保护用户隐私方面做了很大的加强,以下操作都必须经过用户批准授权
    1. 要想获得用户的位置/想访问用户的通讯录、日历、相机、相册等等
    2. 当想访问用户的隐私信息时,系统会自动弹出一个对话框让用户授权
    3. 一旦用户选择了Don’t Allow,意味着你的应用以后就无法使用定位功能
  2. 开发者必须在Info.plist中设置NSLocationUsageDescription说明定位的目的(Privacy - Location Usage Description)
  3. 前台定位
    1. 创建位置管理者 CLLocationManager并设置代理

       // 创建定位服务管理者
       CLLocationManager *localManager = [[CLLocationManager alloc] init];
       //设置代理
       localManager.delegate = self;
      
    2. 开始定位,调用方法,更新位置信息

       //开始定位用户位置
       [localManager startUpdatingLocation];
      
    3. 在CLLocationManagerDelegate的代理方法中获取用户位置信息

       /* 只要定位的用户位置就会调用(调用频率特别高) */
       - (void)locationManager:(CLLocationManager *)manager
            didUpdateLocations:(NSArray<CLLocation *> *)locations{
       }
      
  4. 后台定位
    1. 条件:
      1. 在前台基础上,勾选后台模式location updates或者直接info.plist文件,添加Required background modes(两者实现同一个操作)
    2. 操作:(两者二选一)
      1. Capabilities -> Background Models -> 选中Location updates 打钩
      2. info.plist操作:

         添加Required background modes(数组类型)->
         App registers for location updates
        

2. iOS>=8.0 && iOS<9.0

  1. 从iOS8.0开始,苹果进一步加强了对用户隐私的保护
    1. 当APP想访问用户的隐私信息时,系统就不再自动弹出一个对话框让用户授权
    2. 解决方案:
      1. 调用iOS8.0的API,主动请求哟公户授权

         //请求允许在前台获取用户位置的授权
         - (void)requestWhenInUseAuthorization API_AVAILABLE(ios(8.0)) API_UNAVAILABLE(macos);
         //请求允许在前后台都能获取用户位置的授权
         - (void)requestAlwaysAuthorization API_AVAILABLE(ios(8.0), macos(10.15)) API_UNAVAILABLE(tvos);
        
      2. 同时务必在info.plist文件中配置对应的键值对,否则以上请求授权方法不生效

         NSLocationWhenInUseUsageDescription 允许在前台获取GPS的描述
         NSLocationAlwaysUsageDescription 允许在前后台获取GPS的描述
        
  2. 前台定位
    1. 创建位置管理者CLLocationManager,并设置代理(略)
    2. 开始定位,调用方法(略)
    3. 在CLLocationManagerDelegate的代理方法中获取用户位置信息(略)
    4. iOS8.0后增加操作:
      1. 主动请求前台定位授权

         [localManager requestWhenInUseAuthorization];
        
      2. 在info.plist中填写对应的key(一定要填key)

         NSLocationWhenInUseUsageDescription 需要您的同意,才能访问您的位置
        
  3. 后台定位
    1. 需要在前台定位基础上,即先将上面4步做完
    2. 两种方案:
      1. 方案一:
        1. 开启后台模式 Location updates
          1. Capabilities -> Background Models -> 选中Location updates 打钩
        2. 特点:
          1. 当在后台获取到用户的位置时,会在顶部显示一个蓝条,提醒用户这个app在不断的获取你的位置信息
          2. 当用户点击了这个蓝条,会打开对应的app
      2. 方案二:
        1. 开启前后台定位授权

           [localManager requestAlwaysAuthorization];
          
        2. 在info.plist中填写对应的key(一定要填key)

           NSLocationAlwaysUsageDescription 需要您的同意,才能访问您的位置
          

3. iOS>+9.0 iOS<11.0

  1. 前台定位
    1. 相对于iOS8.0 前台定位一样,没有变化(略)
  2. 后台定位
    1. 需要在前台定位基础上,即先将上面4步做完
    2. 两种方案
      1. 方案一:
        1. 开启后台模式 Location updates
          1. Capabilities -> Background Models -> 选中Location updates 打钩
        2. iOS9.0后增加操作

           localManager.allowsBackgroundLocationUpdates = YES;
          
        3. 特点:
          1. 当在后台获取到用户的位置时,会在顶部显示一个蓝条,提醒用户这个app在不断的获取你的位置信息
          2. 当用户点击了这个蓝条,会打开对应的app
      2. 方案二:(与iOS8.0一样)

4. iOS>= 11.0

  1. 在之前的基础上在info.plist中增加一个字段即可

     NSLocationAlwaysAndWhenInUseUsageDescription  需要您的同意,才能访问您的位置
    

CLLocationManager

  1. CLLocationManager可以实现地理定位/指南针/区域监听功能
    1. 地理定位: 获取当前的位置信息
    2. 指南针: 获取当前的设备朝向
    3. 区域监听: 判断某个定位是否在一个区域内
  2. CLLocationManager的常用方法

     //开始更新用户位置
     - (void)startUpdatingLocation;
        
     //获取地理位置,只获取一次(iOS>9.0)
     - (void)requestLocation;
        
     //停止更新用户位置
     - (void) stopUpdatingLocation;
        
     //判断当前应用的定位功能是否可用:为了严谨起见,最好在使用定位功能之前判断当前应用的定位功能是否可用
     + (BOOL)locationServicesEnabled;
    
  3. 常见属性

     每隔多少米定位一次
     @property(assign, nonatomic) CLLocationDistance distanceFilter;
        
     定位精确度(越精确就越耗电,定位时间越长)
     @property(assign, nonatomic) CLLocationAccuracy desiredAccuracy;
        
     CLLocationAccuracy常用枚举常量: 
     kCLLocationAccuracyBestForNavigation//最适合导航
     kCLLocationAccuracyBest//最好的
     kCLLocationAccuracyNearestTenMeters//10m
     kCLLocationAccuracyHundredMeters//100m
     kCLLocationAccuracyKilometer//1000m
     kCLLocationAccuracyThreeKilometers//3000m
    
  4. 常用代理方法

     /*
      当调用了startUpdatingLocation方法后,就开始不断地请求、刷新用户的位置,一旦请求到用户位置就会调用代理的下面方法
      只要定位的用户位置就会调用(调用频率特别高)
      locations: 数组里面是CLLocation对象
      */
     - (void)locationManager:(CLLocationManager *)manager
          didUpdateLocations:(NSArray<CLLocation *> *)locations{
         NSLog(@"--------%@",locations);
         //1. 获取位置对象
         CLLocation *location = locations.firstObject;
         //2. 取出经纬度
         CLLocationCoordinate2D coordinate = location.coordinate;
         NSLog(@"纬度为:%f--经度为:-%f",coordinate.latitude,coordinate.longitude);
            
         // 停止定位(省电措施:只要不想用定位服务,就马上停止定位服务)
         [manager stopUpdatingLocation];
     }
     //监听用户授权的状态
     - (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status;
     /*
      CLAuthorizationStatus枚举如下:
      kCLAuthorizationStatusNotDetermined 用户还未决定
      kCLAuthorizationStatusRestricted 访问受限
      kCLAuthorizationStatusDenied 用户拒绝
      kCLAuthorizationStatusAuthorizedAlways 获得了前后台定位授权
      kCLAuthorizationStatusAuthorizedWhenInUse 获得了前台定位授权
      */
        
     //获取位置失败调用
     - (void)locationManager:(CLLocationManager *)manager
     didFailWithError:(NSError *)error;
    

CLLocation

  1. CLLocation用来表示某个位置的地理信息,比如经纬度、海拔等等
  2. 常用属性

     //经纬度
     @property(readonly, nonatomic) CLLocationCoordinate2D coordinate;
     CLLocationCoordinate2D类型是一个结构体:
     struct CLLocationCoordinate2D {
      CLLocationDegrees latitude;//纬度
      CLLocationDegrees longitude;//经度
     };
     //通常用CLLocationCoordinate2DMake(CLLocationDegrees latitude, CLLocationDegrees longitude);函数来创建CLLocationCoordinate2D结构体
        
     //海拔
     @property(readonly, nonatomic) CLLocationDistance altitude;
        
     //路线,航向(取值范围是0.0° ~ 359.9°,0.0°代表真北方向)
     @property(readonly, nonatomic) CLLocationDirection course;
        
     //移动速度(单位是m/s)
     @property(readonly, nonatomic) CLLocationSpeed speed;
        
     //该方法可以计算2个位置之间的距离
     - (CLLocationDistance)distanceFromLocation:(const CLLocation *)location
    

举例使用

地理定位

#import "ViewController.h"
#import <CoreLocation/CoreLocation.h>
@interface ViewController ()<CLLocationManagerDelegate>
@property (nonatomic,strong) CLLocationManager *localManager;
@end

@implementation ViewController
-(CLLocationManager *)localManager{
    if (_localManager == nil) {
        //1. 创建定位服务管理者
        _localManager = [[CLLocationManager alloc] init];;
        _localManager.delegate = self;
        //每隔多少米定位一次
        _localManager.distanceFilter = 100;
        //定位精度
        _localManager.desiredAccuracy = kCLLocationAccuracyBest;
        //iOS8.0+ 前台/前后台定位
        [_localManager requestWhenInUseAuthorization];
        [_localManager requestAlwaysAuthorization];
        }
    return _localManager ;
}


-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    //开始定位用户位置
    [self.localManager startUpdatingLocation];
//    if (@available(iOS 9.0, *)) {
//        [self.localManager requestLocation];
//    } else {
//        // Fallback on earlier versions
//    }
}
/*
 只要定位的用户位置就会调用(调用频率特别高)
 locations: 数组里面是CLLocation对象
 
 */
- (void)locationManager:(CLLocationManager *)manager
     didUpdateLocations:(NSArray<CLLocation *> *)locations{
    NSLog(@"--------%@",locations);
    //1. 获取位置对象
    CLLocation *location = locations.lastObject;
    //2. 取出经纬度
    CLLocationCoordinate2D coordinate = location.coordinate;
    NSLog(@"纬度为:%f--经度为:-%f",coordinate.latitude,coordinate.longitude);
    
//    // 停止定位(省电措施:只要不想用定位服务,就马上停止定位服务)
//    [manager stopUpdatingLocation];
}

//检查用户授权的状态
-(void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status{
}
//定位失败时调用
-(void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error{
    
}
@end

指南针

  1. 思路分析:
    1. 获取设备朝向
      1. 磁北方向: magneticHeading(常用)
      2. 真北方向: trueHeading
    2. 让图片逆向旋转同样的角度
    3. 注意: 指南针只是获取用户的设备朝向,并没有涉及到用户的隐私,因此不需要用户进行定位授权
     #import "ViewController.h"
     #import <CoreLocation/CoreLocation.h>
     @interface ViewController ()<CLLocationManagerDelegate>
     @property (weak, nonatomic) IBOutlet UIImageView *imageView;
     @property (nonatomic,strong) CLLocationManager *locationManager;
     @end
        
     @implementation ViewController
     -(CLLocationManager *)locationManager{
         if (_locationManager == nil) {
             _locationManager = [[CLLocationManager alloc] init];
             _locationManager.delegate =self;
         }
         return _locationManager ;
     }
        
     - (void)viewDidLoad {
         [super viewDidLoad];
         //1. 创建位置管理者
         if (CLLocationManager.headingAvailable) {
             [self.locationManager startUpdatingHeading];
         }else{
             NSLog(@"当前磁力计设备不支持!!!");
         }
     }
     - (void)locationManager:(CLLocationManager *)manager
            didUpdateHeading:(CLHeading *)newHeading{
         NSLog(@"---------");
         //1. 拿到当前设备朝向
         //真正的北方: trueHeading
         //磁北方向
        CLLocationDirection angle = newHeading.magneticHeading;
         //2. 让图片反向旋转
         //把角度转换为弧度
         CGFloat hudu = angle/180 * M_PI;
         [UIView animateWithDuration:0.5 animations:^{
             self.imageView.transform = CGAffineTransformMakeRotation(-hudu);
         }];
     }
    

区域监听

  1. 使用情况: 设定一个圆形(或其他形状)区域,通过获取用户的位置,当用户进入或者离开该区域时,调用相应的代理方法

     #import "ViewController.h"
     #import <CoreLocation/CoreLocation.h>
     @interface ViewController ()<CLLocationManagerDelegate>
     @property (nonatomic,strong) CLLocationManager *locationManager;
     @end
        
     @implementation ViewController
     -(CLLocationManager *)locationManager{
         if (_locationManager == nil) {
             _locationManager = [[CLLocationManager alloc] init];
             _locationManager.delegate =self;
             //要想进行区域监听,必须进行定位授权
             //iOS8.0+ 前台/前后台定位
             [_locationManager requestWhenInUseAuthorization];
             [_locationManager requestAlwaysAuthorization];
         }
         return _locationManager ;
     }
        
     - (void)viewDidLoad {
         [super viewDidLoad];
         //1. 创建区域
         CLLocationCoordinate2D center =  CLLocationCoordinate2DMake(22.33, 122.44);
         CLLocationDistance distance = 1000;
        
         if (distance>self.locationManager.maximumRegionMonitoringDistance) {
             NSLog(@"监听区域超过了最大值");
             distance = self.locationManager.maximumRegionMonitoringDistance;
         }
         CLRegion *region = [[CLCircularRegion alloc] initWithCenter:center radius:distance identifier:@"eeee"];
         //class: 监听的区域时什么种类: 圆形(CLCircularRegion)/其他
         if ([CLLocationManager isMonitoringAvailableForClass:[CLCircularRegion class]]) {
             //2. 开始监听区域,停止监听:[self.locationManagerstopMonitoringForRegion:region];
             [self.locationManager startMonitoringForRegion:region];
             //2.1 请求当前区域的状态(程序一启动,先查看当前位置是不是在对应区域内,调用对应的代理方法)
             [self.locationManager requestStateForRegion:region];
         }else{
             NSLog(@"不能监听区域");
         }
            
     }
        
     //进入区域时调用
     -(void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region{
         NSLog(@"进入了区域");
     }
     //离开区域时调用
     -(void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region{
         NSLog(@"离开了区域");
     }
     /*
      情况:
      因为上面的两个代理方法,只有在动作即区域改变的时候才会调用,但是在有些时候,程序加载之后,我就要展示当前位置是否在区域内,这就需要这个代理方法了
      该代理方法跟: [self.locationManager requestStateForRegion:region];结合使用
      */
     -(void)locationManager:(CLLocationManager *)manager didDetermineState:(CLRegionState)state forRegion:(CLRegion *)region{
         switch (state) {
             case CLRegionStateUnknown:
                 NSLog(@"不知道");
                 break;
             case CLRegionStateInside:
                 NSLog(@"在区域内");
                 break;
             case CLRegionStateOutside:
                 NSLog(@"在区域外");
                 break;
             default:
                 break;
         }
     }
     @end
    

CLGeocoder

  1. 使用CLGeocoder可以完成“地理编码”和“反地理编码”
    1. 地理编码:根据给定的地名,获得具体的位置信息(比如经纬度、地址的全称等)
    2. 反地理编码:根据给定的经纬度,获得具体的位置信息
  2. 地理编码方法

     - (void)geocodeAddressString:(NSString *)addressString completionHandler:(CLGeocodeCompletionHandler)completionHandler;
    
  3. 反地理编码方法

     - (void)reverseGeocodeLocation:(CLLocation *)location completionHandler:(CLGeocodeCompletionHandler)completionHandler;
    
  4. CLGeocodeCompletionHandler
    1. 当地理\反地理编码完成时,就会调用CLGeocodeCompletionHandler这个block

       typedef void (^CLGeocodeCompletionHandler)(NSArray *placemarks, NSError *error);
      
    2. 参数分析:

      1. 这个block传递2个参数
      2. error :当编码出错时(比如编码不出具体的信息)有值
      3. 里面装着CLPlacemark对象(为什么是一个数组? 因为同一个地名全国可能有几个地方)
  5. CLPlacemark
    1. CLPlacemark的字面意思是地标,封装详细的地址位置信息
    2. 常用属性如下

       //地理位置
       @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;
      
  6. 代码举例:

     #import "ViewController.h"
     #import <CoreLocation/CoreLocation.h>
     @interface ViewController ()
     @property (nonatomic,strong) CLGeocoder *geocoder;
     #pragma mark - 地理编码
        
     @property (weak, nonatomic) IBOutlet UITextField *addressField;
     @property (weak, nonatomic) IBOutlet UILabel *longitudeLabel;
     @property (weak, nonatomic) IBOutlet UILabel *latitudeLabel;
     @property (weak, nonatomic) IBOutlet UILabel *detailAddressLabel;
        
     #pragma mark - 反地理编码
     @property (weak, nonatomic) IBOutlet UITextField *longtitudeField;
     @property (weak, nonatomic) IBOutlet UITextField *latitudeField;
     @property (weak, nonatomic) IBOutlet UILabel *reverseDetailAddressLabel;
        
     @end
        
     @implementation ViewController
     -(CLGeocoder *)geocoder{
         if (_geocoder == nil) {
             _geocoder = [[CLGeocoder alloc] init];
         }
         return _geocoder ;
     }
     //地理编码
     - (IBAction)geocode:(id)sender {
         if (!self.addressField.text.length) {
             return;
         }
         [self.geocoder geocodeAddressString:self.addressField.text completionHandler:^(NSArray<CLPlacemark *> * _Nullable placemarks, NSError * _Nullable error) {
             if (error) {//有错误
                 NSLog(@"");
                 self.detailAddressLabel.text = @"您查找的地址不存在!";
             }else{
                 CLPlacemark *placemark = placemarks.firstObject;
                 self.longitudeLabel.text = [NSString stringWithFormat:@"%f",placemark.location.coordinate.longitude];
                 self.latitudeLabel.text = [NSString stringWithFormat:@"%f",placemark.location.coordinate.latitude];
                 self.detailAddressLabel.text = [NSString stringWithFormat:@"%@-%@-%@",placemark.country,placemark.administrativeArea,placemark.locality];
                 NSLog(@"%@",placemark.addressDictionary);
             }
         }];
     }
        
     //反地理编码
     - (IBAction)reverseGeocode:(id)sender {
         // 1.包装位置
         CLLocationDegrees latitude = [self.latitudeField.text doubleValue];
         CLLocationDegrees longitude = [self.longtitudeField.text doubleValue];
         CLLocation *loc = [[CLLocation alloc] initWithLatitude:latitude longitude:longitude];
         [self.geocoder reverseGeocodeLocation:loc completionHandler:^(NSArray<CLPlacemark *> * _Nullable placemarks, NSError * _Nullable error) {
             if (error) {//有错误
                 NSLog(@"");
                 self.reverseDetailAddressLabel.text = @"您查找的地址不存在!";
             }else{
                 CLPlacemark *placemark = placemarks.firstObject;
                 self.reverseDetailAddressLabel.text = [NSString stringWithFormat:@"%@-%@-%@",placemark.country,placemark.administrativeArea,placemark.locality];
                 NSLog(@"%@",placemark.addressDictionary);
             }
         }];
     }
        
     @end
    
Table of Contents