美文网首页ios专题
CoreLocation 定位

CoreLocation 定位

作者: 珍此良辰 | 来源:发表于2016-05-28 08:30 被阅读1015次

    前言:
    本章会使用OC和Swift分别进行实现,需要了解Swift的小伙伴可以翻一下之前的博文

    LBS和SoloMo(索罗门)

    • LBS:基于位置的服务,根据定位展示周边美食、景点等信息(全称:Location Bassed Service)
    • SoloMo:将位置社交、本地、移动化(全称:Soclal Local Moblle)
      • 社交化:在APP内加入一些社交元素,进行位置分享等
      • 本地化:基于LBS周边的搜索等服务
      • 移动化:基于3G\4G网络在移动APP上的服务

    CoreLocation介绍

    • 定位已经可以说是现在APP的主流,没有定位功能的APP都不好意思和大家见面,作为APP的重要组成部分,其实使用也非常简单,本章就苹果的CoreLocation框架进行简单分析和使用
    • CoreLocation主要功能
      • 地理定位:获取用户所以在区域,得到相应的经纬度或者海拔等一些地理信息
      • 地理编码:根据详细的地址转换为经纬度信息
      • 反地理编码:根据经纬度信息转换成具体地址
      • 区域监听:指定一个区域,当用户进入或者离开这个区域,我们都可以监听到对应信息
      • 一般MapKit和一起使用,因为Mapkit就是基于CoreLocation进行开发的,所以MapKit能进行定位也能展示地图,以后会就MapKit进行详解

    distanceFilter(距离过滤)和 desiredAccuracy(定位精确度)属性

    • distanceFilter(距离过滤):最新位置距上次位置之间距离大于这个值,就会告诉通过代理告诉外界

      • 默认距离KCLDistanceFilterNone (值为-1,因为小于0,所以会一直打印)
      • 单位:米
    • desiredAccuracy(定位精确度):定位精确度越高,定位时间就越长,也就越耗电

      • kCLLocationAccuracyBestForNavigation // 最适合导航
      • kCLLocationAccuracyBest // 最好的
      • kCLLocationAccuracyNearestTenMeters; // 附近10米
      • kCLLocationAccuracyHundredMeters; // 附近100米
      • kCLLocationAccuracyKilometer; // 附近1000米
      • kCLLocationAccuracyThreeKilometers; // 附近3000米

    iOS8之前定位

    • 在XCode5之前我们需要用到的框架修需要手动导入(这边使用的是XCode7.3,有冲突的请进行相应调整)
    • CoreLocation框架的主头文件#import <CoreLocation/CoreLocation.h>
    • 定位前需要先获取CLLocationManager对象
    • 从iOS6开始,想要获取用户的隐私(通讯录、日历、相机、定位、相册等),系统会自动弹框请求授权
    • 在iOS8.0之前,为了提高用户点击允许授权的机率,通常会在info.plist中配置对应的key(Privacy - Location Usage Description)用来说明定位目的

    OC:

    // 为了全局只使用一个位置管理者,我们先对CLLocationManager进行懒加载
    - (CLLocationManager *)locationM {
        if (_locationM == nil) {
            
            // 创建位置管理者
            _locationM = [[CLLocationManager alloc] init];
            
            // 设置代理
            _locationM.delegate = self;
           
        }
        return _locationM;
    }
    
    // 在按钮点击事件中开启定位服务
        // start:开启服务 stop:关闭服务
        // 一旦调用这个方法,就会不断的调用用户信息(因为distanceFilter属性的默认值为-1)
        // 基本定位(基于Wifi/GPS)
        [self.locationM startUpdatingLocation];
        
        
    // 这个方法是用来监听重大位置改变(因为基站与基站之间相距大所以这个方法会通过基站进行定位,前提是有电话模块的支持)
    //    [self.locationM startMonitoringSignificantLocationChanges];
    
    // 先遵守CLLocationManagerDelegate协议,实现下面代理方法
    
    - (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations {
    
        NSLog(@"已定位到");
        
    // 定位是非常频繁的,所以获取到用户信息后,最好马上关闭停止定位,以达到省电效果,在适当的时候再重新打开定位
        [manager stopUpdatingLocation];
        self.locationM = nil;
        
    }
    
    

    Swift:

    // MARK:- 懒加载
        private lazy var locationM : CLLocationManager = {
            // 创建位置管理者
            let locationM = CLLocationManager()
            
            // 设置代理
            locationM.delegate = self
           
            return locationM
           
        }()
    
        override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
            
            // 使用位置管理者获取用户位置信息
            // 根据苹果的习惯,一般方法命中带ing(现在进行时),说明一旦执行这个方法,系统就会不断的调用这个方法
            // 默认情况下只会在前台进行定位,如果在后台也想要获取用户的位置,需要开启后台模式 location updates
            locationM.startUpdatingLocation()
            
        }
    
    // MARK:- CLLocationManagerDelegate
    extension ViewController : CLLocationManagerDelegate {
        
        // manager :  位置管理者
        // locations : 位置数组
        func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
            print("已定位到")
            //  关闭定位
            manager.stopUpdatingLocation()
        }
    
    

    startMonitoringSignificantLocationChanges(重大位置改变监听)

    • 当位置发生较大变化后会调用这个服务(基于基站定位,所以必须要有电话模块)
      • 优势:当APP被完全关闭后,也可以接收到位置通知,并且让APP进入后台处理,耗电量小
      • 劣势:定位精度相对于标准定位服务较低,更新的频率根据当前位置附近的基站密度决定

    后台继续定位

    如果想要在后台继续进行定位,需要打开后台的定位模式

    开启后台定位模式.gif

    拓展:

    • 标准的定位服务(基于GPS/Wifi/基站的定位服务)
      • 程序被完全关闭后就无法再获取位置信息
    • 显著位置变化定位服务(基于基站的定位服务,设备必须有电话模块支持)
      • 当APP被完全关闭后,也可以接收到位置通知,并且让APP进入后台处理
      • 定位精度相对于标准定位服务较低,耗电量小,更新的频率根据当前位置附近的基站密度决定

    iOS8之后定位

    • 从iOS8开始,苹果进一步加强了对用户隐私的保护,当APP想范围用户隐私信息的时候,系统不再自动弹出对话框让用户授权,为了能让系统自动弹出用户授权界面,需要进行下面设置

      • 解决方案:调用iOS8的API,主动请求用户授权


      
      // 注意:根据官方文档的解释,在使用下面2个方法的时候,如果不在info.plist中配置NSLocationWhenInUseUsageDescription这个key,那么方法都不会生效
      // 请求前台定位授权
          - (void)requestWhenInUseAuthorization
      
      // 注意:根据官方文档的解释,在使用下面2个方法的时候,如果不在info.plist中配置`NSLocationAlwaysUsageDescription`这个key,那么方法都不会生效
      // 请求前后台定位授权
          - (void)requestAlwaysAuthorization
      
      

    OC:

    - (CLLocationManager *)manager
    {
        if (_manager == nil) {
            _manager = [[CLLocationManager alloc] init];
            
            _manager.delegate = self;
            
            // 需要注意的是,必须在info.plist文件中配置’NSLocationWhenInUseUsageDescription‘这个key,否则下面方法无效(官方注释有提到)
            // 请求前台授权
            [_manager requestWhenInUseAuthorization];
            // 请求前后台授权
    //        [_manager requestAlwaysAuthorization];
        }
        
        return _manager;
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        [self.manager startUpdatingLocation];
    }
    
    - (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation
    {
        NSLog(@"定位到了");
        
        [self.manager stopUpdatingLocation];
    }
    
    
    

    swift:

    class ViewController: UIViewController {
    
        lazy var locationMgr : CLLocationManager = {
            
            let locationMgr = CLLocationManager()
            
            locationMgr.delegate = self
            
            // 记得设置相应的授权请求Key
            // 请求前台定位授权
            locationMgr.requestWhenInUseAuthorization()
            // 请求前后台定位授权
            locationMgr.requestAlwaysAuthorization()
            
            return locationMgr
        
        }()
        
        override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
            
            // 开启定位
            locationMgr.startUpdatingLocation()
        }
    
    
    }
    
    //MARK: - CLLocationManager代理
    extension ViewController : CLLocationManagerDelegate {
        
        // 当定位到位置后调用
        func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
            
            print("定位到了")
            
            manager.stopUpdatingLocation()
        }
    }
    
    
    

    定位适配

    • 一般我们适配版本都会判断当前设备的版本,然后再进行相应的适配操作,这边就介绍另一种比较简单的适配方式
      • 通过respondsToSelector:方法来判断方法是否可响应,可以的话再执行

    OC:

    if ([_manager respondsToSelector:@selector(requestWhenInUseAuthorization)]) {
                
          // 需要注意的是,必须在info.plist文件中配置’NSLocationWhenInUseUsageDescription‘这个key,否则下面方法无效(官方注释有提到)
          // 请求前台授权
          [_manager requestWhenInUseAuthorization];
    }
            
    if ([_manager respondsToSelector:@selector(requestAlwaysAuthorization)]) {
                
         // 请求前后台授权(无论是否开启后台模式都可以获取位置信息,并且不会出现蓝条提示)
          [_manager requestAlwaysAuthorization];
    }
    
    
    

    swift:

    // 记得设置相应的授权请求Key
            // 根据当前系统版本适配
            // 当前版本是8.0及以上
            if #available(iOS 8.0, *) {
                // 请求前台定位授权
                locationMgr.requestWhenInUseAuthorization()
            }
            if #available(iOS 8.0, *) {
                // 请求前后台定位授权
                locationMgr.requestAlwaysAuthorization()
            }
    
    
    
    • iOS9定位变化
      • 前台定位于iOS8无变化
        • 后台定位
          • 方法一:在前台定位授权基础上,勾选后台模式location updates之后,需要额外设置属性allowsBackgroundLocationUpdates = YES
          • 方法二:直接请求前后台定位授权,设置属性allowsBackgroundLocationUpdates = YES,开启后台模式

    定位服务未开启或者被用户真正拒绝情况下的情况处理

    • iOS8之前,需要将开启授权的截图展示给用户,让用户根据截图去开启授权
    • iOS8之后,会自动弹出设置窗口,让用户选择是否需要开启授权
      • iOS开始我们可以根据URL直接跳转到相应的设置界面

    OC:

    //  当授权状态发生改变时调用
    - (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status
    {
        
        switch (status) {
            case kCLAuthorizationStatusNotDetermined:
                NSLog(@"用户未选择");
                break;
            // 暂时没用,应该是苹果预留接口
            case kCLAuthorizationStatusRestricted:
                NSLog(@"受限制");
                break;
            // 真正被拒绝、定位服务关闭等影响定位服务的行为都会进入被拒绝状态
            case kCLAuthorizationStatusDenied:
                
                if ([CLLocationManager locationServicesEnabled]) { // 定位服务开启
                    NSLog(@"真正被用户拒绝");
                    
                    //  跳转到设置界面
                    NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
                    
                    if ([[UIApplication sharedApplication] canOpenURL:url]) {   // url地址可以打开
                        [[UIApplication sharedApplication] openURL:url];
                    }
                } else {
                    NSLog(@"服务未开启");
                }
                
                break;
            case kCLAuthorizationStatusAuthorizedAlways:
                NSLog(@"前后台定位授权");
                break;
            case kCLAuthorizationStatusAuthorizedWhenInUse:
                NSLog(@"前台定位授权");
                break;
                
            default:
                break;
        }
    }
    
    

    swift:

    func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
            
            switch status {
            case .NotDetermined:
                print("用户未选择")
            case .Restricted:
                print("受限制")
            case.Denied:
                print("被拒绝")
                if CLLocationManager .locationServicesEnabled() { // 定位服务开启
                    print("用户真正拒绝")
                    
                    // 跳转到设置界面
                    if #available(iOS 8.0, *) {
                        let url = NSURL(string: UIApplicationOpenSettingsURLString)
                        if UIApplication.sharedApplication().canOpenURL(url!) {
                            UIApplication.sharedApplication().openURL(url!)
                        }
                    }
                } else {
                    print("服务未开启")
                }
            case .AuthorizedAlways:
                print("前后台定位授权")
            case .AuthorizedWhenInUse:
                print("前台定位授权")
            }
        }
    
    
    

    获取位置信息

    // 获取当前位置信息
    - (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations
    {
        // locations内的元素是按时间顺序排列,所以要获取最新的位置信息直接取locations数组内的最后一个元素即可(苹果官方文档注释)
        NSLog(@"%@", [locations lastObject]);
    }
    
    

    结果:维度、经度、海拔(负值表示当前海拔无效)速度(负)航向(从0~359.9) 位置时间

    当前最新位置信息
    • 根据获取的位置信息计算用户行走方向,行走距离,偏移角度
      • coordinate:经纬度信息
      • altitude:海拔
      • horizontalAccuracy:水平方向精度,值为负数时,表示无效
      • verticalAccuracy:判断海拔是否为负数,负数无效
      • course:航向(0~359.9)
      • floor:楼层(使用的楼层需要注册,否则无法使用)
      • distanceFromLocation:计算2点之间的物理直线距离


    OC:
    
    ```
        // 获取当前位置信息
        - (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations
    {
        /*
        * coordinate:经纬度信息
        * altitude:海拔
        * horizontalAccuracy:水平方向精确度,值如果小于0,代表位置数据无效
        * verticalAccuracy:判断海拔是否为负数,负数无效
        * floor:楼层,使用的楼层需要注册,否则无法使用
        * course:航向(0~359.9)(这里的0表示的是正北不是磁北)
        * distanceFromLocation:计算2各店之间物理直线距离
        */
    
        //  获取当前位置信息
        CLLocation *locationC = locations.lastObject;
    
        // 判断水平数据是否有效
        if (locationC.horizontalAccuracy < 0) { // 负数表示无效
            return;
        }
        // 计算行走方向(北偏东,东偏南,南偏西,西偏北)
        NSArray *courseAry = @[@"北偏东", @"东偏南", @"南偏西", @"西偏北"];
        // 将当前航向值/90度会得到对应的值(0,1,2,3)
        NSInteger i = locationC.course / 90;
        // 取出对应航向
        NSString *courseStr = courseAry[i];
    
        // 计算偏移角度
        NSInteger angle = (int)locationC.course % 90;
        // 判断是否为正方向
        // 对角度取余,为0表示正
        if (angle == 0) {
        
            // 截取字符串第一个字
            courseStr = [courseStr substringToIndex:1];
            // 拼接字符串
            courseStr = [@"正" stringByAppendingString:courseStr];
        }
    
        // 计算移动多少米
        CGFloat distance = [locationC distanceFromLocation:self.lastLocation];
    
        // 记录上次距离
        self.lastLocation = locationC;
    
        NSLog(@"向 %@ 方向走了 %lf 米偏移角度 %ld 度", courseStr, distance, angle);
    }
    
    ```
    
    swift:
    
    ```
    // 当定位到位置后调用
    func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        
        // 获取用户当前最新位置
        let locationC = locations.last
        
        // 判断水平数据是否有效
        if locationC?.horizontalAccuracy < 0 { // 负数表示无效
            return
        }
        
        // 计算行走方向(北偏东,东偏南,南偏西,西偏北)
        let courseAry = ["北偏东", "东偏南", "南偏西", "西偏北"]
        // 将当前航向值/90度会得到相应的值(0,1,2,3)
        let i = Int((locationC?.course)! / 90)
        // 取出对应航向
        var courseStr = courseAry[i]
        
        // 计算偏移角度
        let angle = Int((locationC?.course)! % 90)
        // 判断是否为正方向
        // 对角度取余,为0就表示正
        if Int(angle) == 0 {
            // 截取字符串第一个字
            courseStr = (courseStr as NSString).substringToIndex(1)
        }
        
        // 确定移动距离
        let lastLoc = lastLocation ?? locationC
        let distance = locationC?.distanceFromLocation(lastLoc!)
        lastLocation = locationC
        
        // 拼接字符串
        print("向\(courseStr)方向走了\(distance!)米偏移角度\(angle)")
    }
    
    
    ```
    

    区域监听

    • 区域监听就是根据需求指定一块区域,当用户持设备进入或离开指定区域,我们都可以监听到
    • iOS8开始,想要做区域监听,必须请求位置授权(因为区域监听原理就是获取用户位置,然后判断位置是否在设定的区域内,涉及到用户隐私)

    OC:

        - (CLLocationManager *)manager
    {
        if (!_manager) {
            _manager = [[CLLocationManager alloc] init];
            _manager.delegate = self;
            
            // 请求用户授权区域监听
            if ([[UIDevice currentDevice].systemVersion floatValue] >= 8.0) {
                [_manager requestAlwaysAuthorization];
            }
        }
        
        return _manager;
    }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        // 判断区域监听是否可用
        if ([CLLocationManager isMonitoringAvailableForClass:[CLCircularRegion class]]) {
            return;
        }
        
        // 创建一个区域
        // 确定圆心
        CLLocationCoordinate2D center = CLLocationCoordinate2DMake(21.23, 123.345);
        // 确定半径
        CLLocationDistance distance = 1000.0;
        // 因为监听区域有最大值,所以要判断下是否超过监听的最大值
        if (distance > self.manager.maximumRegionMonitoringDistance) {
            distance = self.manager.maximumRegionMonitoringDistance;
        }
        CLCircularRegion *region = [[CLCircularRegion alloc] initWithCenter:center radius:distance identifier:@"123"];
        
        // 开始监听区域
        [self.manager startMonitoringForRegion:region];
    }
    
    
    // 进入区域时
    - (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region
    {
        NSLog(@"进入区域%@",region.identifier);
    }
    
    // 离开区域时
    - (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region
    {
        NSLog(@"离开区域%@",region.identifier);
    }
    
    // 但外界调用请求某个指定区域的状态时
    - (void)locationManager:(CLLocationManager *)manager didDetermineState:(CLRegionState)state forRegion:(CLRegion *)region
    {
        if (state == CLRegionStateUnknown)
        {
            NSLog(@"未识别");
        }
        if (state == CLRegionStateInside) {
            NSLog(@"在区域内");
        }
        if (state == CLRegionStateOutside) {
            NSLog(@"在区域外");
        }
    }
        
    

    swift:

    lazy var locationMgr : CLLocationManager = {
            
            let locationMgr = CLLocationManager()
            
            locationMgr.delegate = self
            
            // 记得设置相应的授权请求Key
            // 当前版本是8.0及以上
            if #available(iOS 8.0, *) {
                // 请求前后台定位授权
                locationMgr.requestAlwaysAuthorization()
            }
            
            return locationMgr
        
        }()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            // 创建一个区域
            // 确定圆心
            let center = CLLocationCoordinate2DMake(21.23, 123.345)
            // 确定半径
            var distance : CLLocationDistance = 1000
            // 因为监听区域有最大值,索引先判断是否超过了监听区域的最大值
            if distance > locationMgr.maximumRegionMonitoringDistance {
                distance = locationMgr.maximumRegionMonitoringDistance
            }
            let region = CLCircularRegion(center: center, radius: distance, identifier: "123")
            
            // 判断取余监听是否可用
            if CLLocationManager.isMonitoringAvailableForClass(region.classForCoder) {
                // 开始监听区域
                locationMgr.startMonitoringForRegion(region)
            }
            
            
        }
    
    
        // 进入区域
        func locationManager(manager: CLLocationManager, didEnterRegion region: CLRegion) {
            print("进入监听区域")
        }
        
        // 离开区域
        func locationManager(manager: CLLocationManager, didExitRegion region: CLRegion) {
            print("离开监听区域")
        }
        
        // 区域状态改变
        func locationManager(manager: CLLocationManager, didDetermineState state: CLRegionState, forRegion region: CLRegion) {
            if state == .Unknown {
                print("未识别")
            }
            if state == .Inside {
                print("在区域内")
            }
            if state == .Outside {
                print("在区域外")
            }
        }
    
    
    
    • 注意:
      • 必须请求用户定位授权
      • 使用前先判断区域监听是否可用
      • 判断区域半径是否大于最大监听区域,如果大于最大监听区域范围,则无法监听成功

    地理编码和反地理编码

    • 地理编码:指根据地质关键字,将其转换成对应的经纬度等信息
    • 反地理编码:指根据经纬度信息,将其转换成对应的省市区等信息
    • CLPlacemark(地表对象)
      • location:CLLocation类型,位置对象的信息,包含经纬度,海拔等
      • region:CLRegion类型,地表对象对应区域
      • addressDictionary:NSDictionary类型,存放省市,街道等信息
      • name:NSString类型,地址全称
      • thoroughfare:NSString类型,街道名称
      • locality:NSString类型,城市名称
      • administrativeArea:NSString类型,省名称
      • country:NSString类型,国家名称
    • 注意
      • 必须联网
      • 有时候反地理编码时会找不到对应信息,需要尝试更换经纬度

    OC:

    CLGeocoder *geocoder = [[CLGeocoder alloc] init];
        // 地理编码
        [geocoder geocodeAddressString:@"福建省厦门市" completionHandler:^(NSArray<CLPlacemark *> * _Nullable placemarks, NSError * _Nullable error) {
            
            CLPlacemark *placeM = [placemarks lastObject];
            
            NSLog(@"维度:%@ -- 经度:%@", @(placeM.location.coordinate.latitude).stringValue, @(placeM.location.coordinate.longitude).stringValue);
        }];
        
        CLGeocoder *geocoder = [[CLGeocoder alloc] init];
        // 反地理编码
        CLLocationDegrees latitude = 24.490474;
        CLLocationDegrees longitude = 118.11022;
        CLLocation *location = [[CLLocation alloc] initWithLatitude:latitude longitude:longitude];
        [geocoder reverseGeocodeLocation:location completionHandler:^(NSArray<CLPlacemark *> * _Nullable placemarks, NSError * _Nullable error) {
            NSLog(@"地址:%@", [placemarks firstObject].name);
        }];
    
    
    

    swift:

    let geocoder = CLGeocoder()
            // 地理编码
            geocoder.geocodeAddressString("福建省厦门市") { (placemarks, error) in
                
                let placeM = placemarks?.last
                
                print("维度\(placeM?.location?.coordinate.latitude) -- 经度\(placeM?.location?.coordinate.longitude)")
            }
            
            // 反地理编码
            let latitude : CLLocationDegrees = 24.490474
            let longitude : CLLocationDegrees = 118.11022
            let location = CLLocation(latitude: latitude, longitude: longitude)
            
            geocoder.reverseGeocodeLocation(location) { (placemarks, error) in
                print("地址:\(placemarks?.first?.name)")
            }
            
        }
    
    

    先到这,最近太忙,过两天找个时间根据定位做个小项目再分享出来
    小项目地址

    相关文章

      网友评论

        本文标题:CoreLocation 定位

        本文链接:https://www.haomeiwen.com/subject/yjoxdttx.html