CoreLocation、MapKit和地理围栏1

CoreLocation、MapKit和地理围栏

地图和位置信息是最有用的iOS功能,让应用能够提供相关的本地信息,帮助用户找到前进的方向,当前,有帮助用户根据需求查找地点的应用,有帮助用户确定行车路线的应用,有帮助用户使用特殊通勤服务的应用,还有让反复前往一个地点变得趣味盎然的应用。Apple在推出新地图的同事,新增课一些功能强大的特性,开发人员可以利用他们让应用更上一层楼。

iOS提供了两个支持定位和地图的框架。框架Core Location包含的类可以帮助设备确定位置和航向以及基于位置的信息;框架MapKit框架为定位提供了用户界面的支持。Apple地图提供而来地图视图、卫星地体以及2D和3D的混合视图。框架MapKit让开发人员能够管理地图标注和地图覆盖层,其中前者类似于大头针,后者可用于突出位置,路线等。

获取用户位置

要使用CoreLocation获取设备当前的位置,需要执行多个步骤,只有在用户许可的情况下,应用才能够获取设备的当前位置;获取设备位置前,应用还确保设备启动了定位服务。满足这些条件后,应用就可以启动位置请求,并对CoreLocation提供的结果进行分析和使用。

需求和许可

要在应用当只当中使用CoreLocation,需要将框架CoreLocation加入到项目目标,并根据需要导入CoreLocation头文件:#import

同样,要在引用中使用MapKit,需要将框架加入到项目目标,并将所有使用它的类加入MapKit头文件:#import

CoreLocation尊重用户隐私,仅在用户许可时获取设备的当前位置。在应用“设置”的“隐私”部分,可在设备上关闭或开启定位服务,还可以禁止或允许特定应用获取位置信息。

要请求用户允许使用定位服务,应用需要让CLLocationManager开始更新当前位置或者将MKMapView实例的属性ShowsUseLocation设置为YES。如果设备关闭了定位服务,CoreLocation将提醒用户在应用“设置”中开启定位服务,让应用获取到当前位置。

如果位置管理器以前未请求用户允许获取设备的位置,它将显示一个提醒框,请求用户许可。

如果用户请安OK按钮,说明得到了用户许可,位置管理器将获取当前位置。如果用户请按按钮Dow'n Allow禁止获取当前位置,讲调用CLLocationManager的委托ICFLocationManager中响应授权状态变化的方法。

-(void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status{
    /*
     // 用户从未选择过权限
     kCLAuthorizationStatusNotDetermined = 0,
     // 无法使用定位服务,该状态用户无法改变
     kCLAuthorizationStatusRestricted,
     // 用户拒绝该应用使用定位服务,或是定位服务总开关处于关闭状态
     kCLAuthorizationStatusDenied,
     // 这个值已弃用
     kCLAuthorizationStatusAuthorized 
     // 大致是用户允许该程序无论何时都可以使用地理信息
     kCLAuthorizationStatusAuthorizedAlways 
     // 大致是用户同意程序在可见时使用地理位置
     kCLAuthorizationStatusAuthorizedWhenInUse 
     */
    [self.locationManager stopUpdatingLocation];
    if (status == kCLAuthorizationStatusDenied) {
        NSString *errorString = @"没有获得用户授权";
        //NSLocalizedDescriptionKey本地描述关键字
        NSDictionary *errorInfo = @{NSLocalizedDescriptionKey:errorString};
        NSError *error = [NSError errorWithDomain:@"ICFLocationErrorDomain" code:1 userInfo:errorInfo];

        [self setLocationError:error];
//        [self getLocationWithCompletionBlock:nil];
    }
    if (status == kCLAuthorizationStatusAuthorizedAlways) {
        [self.locationManager startUpdatingLocation];
        [self setLocationError:nil];
    }
}

检查定位服务是否已经开启

要直接检查设备是否开启了定位服务,可使用CLLocationManager的类方法locationServicesEnabled。

    if ([CLLocationManager locationServicesEnabled]) {
        ICFLocationManager *appLocationManager = [ICFLocationManager sharedLocationManager];

        [appLocationManager.locationManager startUpdatingLocation];
    }else{
        NSLog(@"用户还没开启定位");
    }

使用这个类方法,可以让应用根据能否获取当前位置而采取不同的措施。在使用位置的应用中,应在用户禁止获取当前位置时候采取妥善措施,并清除告诉用户,如果他想让应用获得当前位置该怎么做。

开始位置请求

获取使用定位服务的许可后,便可以使用CLLocationManager实例来获取当前位置了。在例子中使用ICFLocationManager负责集中管理位置功能。它为应用管理者一个CLLocationManager实例。在ICFLocationManager类的方法init中,创建一个CLLocationManager实例,并根据定位需求对其进行定制

        self.locationManager = [[CLLocationManager alloc]init];

        /*
         定位精度:
         kCLLocationAccuracyBest:最精确定位
         CLLocationAccuracy kCLLocationAccuracyNearestTenMeters:十米误差范围
         kCLLocationAccuracyHundredMeters:百米误差范围
         kCLLocationAccuracyKilometer:千米误差范围
         kCLLocationAccuracyThreeKilometers:三千米误差范围
         */
        self.locationManager.desiredAccuracy = kCLLocationAccuracyBest;
        self.locationManager.distanceFilter = 100.f;
        self.locationManager.delegate = self;

可以设置CLLocationManager的多个属性,以及制定它如何管理当前位置。通过设置精度属性desiredAccuracy,应用可以告诉CLLocation,该以缩短电池续航时间为代价尽可能提高精度,还是以延长电池续航时间而使用较低精度。使用较低精度的时候,还可以缩短获取当前位置所需要的时间。通过设置属性distanceFilter,可以告诉CLLocationManager移动多长距离后才出发新的位置时间;这对微调基于位置变化的功能很有帮助。最后,给CLLocationManager指定了委托,这使得可以独特的方式响应位置时间和授权状态的变化。为获取位置做好准备后,让位置管理器开始更新位置。

        ICFLocationManager *appLocationManager = [ICFLocationManager sharedLocationManager];

        [appLocationManager.locationManager startUpdatingLocation];

CLLocationManager将根据指定的参数在需要时候利用GPS或/和Wifi确定当前位置。应该事先两个委托方法,他们分别处理如下情形:位置管理器更新了当前位置或者无法更新当前的位置。获取位置后,将调用方法locationManager:didUpdateLocations:

//获取位置后将会调用此方法
-(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations{
    CLLocation *lastLocation = [locations lastObject];
    if (lastLocation.horizontalAccuracy < 0) {
        return;
    }

    [self setLocation:lastLocation];
    [self setHasLocation:YES];
    [self setLocationError:nil];

    [self getLocationWithCompletionBlock:nil];
}

位置管理器可能通过数组locations提供多个位置,其中最后一个对象是最新的位置。位置管理器还可能没有开始获取位置时,就快速返回GPS检测到的最后位置;在这种情况下,如果GPS已关闭且设备发生了移动,返回的位置就会很不准确。这个方法检查位置的精度,如果精度值为负,就忽略返回的位置。如果返回的位置相当准确。这个方法就存储他并且执行结束块。请注意,在逐步获取准确位置期间,位置管理器可能多次调用这个方法,编写这个方法的时候就要考虑到这一点。

//当位置未能获取位置,它将调用此方法.
-(void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error{
    [self.locationManager stopUpdatingLocation];
    [self setLocationError:error];
    [self getLocationWithCompletionBlock:nil];
}

如果位置管理器未能获取位置。它将调用方法locationMamager:didFailWithError:。导致错误的原因可能是应为未得到用户的许可,也可能是由于GPS或Wifi不可用。发生错误时,应用命令位置管理器停止更新当前位置,捕获错误并执行结束块(让请求当前位置的代码能够妥善的处理错误)。

位置管理器委托可以监视航向变化,这很有用。例如:可以使用这些信息在地图上标出用户中的前进路线相对于正确路线的偏差。要获取航向信息,需要让位置管理器对其进行监视。还可以设置一个可选的筛选器,这样航向变换小于指定的度数时候就不会获取更新。

    //如果发生航向转换小于指定度数将不会获取更新
    if ([CLLocationManager headingAvailable]) {
        [self.locationManager setHeadingFilter:degreeFilter];
        [self.locationManager startUpdatingHeading];
    }

发生航向变化时间时候,将调用委托方法locationManager:didUpdateHeading:。

//发生航向改变时候将调用该方法
-(void)locationManager:(CLLocationManager *)manager didUpdateHeading:(CLHeading *)newHeading{
    NSLog(@"New heading,magnetic:%f",newHeading.magneticHeading);

    NSLog(@"New jeadomg ,true:%f",newHeading.trueHeading);
    NSLog(@"Accuracy:%f",newHeading.headingAccuracy);
    NSLog(@"Timestap:%@",newHeading.timestamp);

}

参数newHeading提供了多项重要信息,其中包括相对于磁北的航向和相对于真北的航向,他们的单位都是度。它还提供了精度,这指出了磁北航向可能偏离多少度。这个值为越少的正数,航向就越准确;如果为负数,就说明航向无效,这个可能是因为存在磁场干扰。时间戳指出了航向是什么时候获取的,应该检查他避免使用过时的航向。

分析和理解位置的数据

位置管理器返回的位置是用CLLocation实例表示的。CLLocation包含多项有关重要位置的信息,首先是用CLLocationCoordinate2D表示精度和维度。

    CLLocationCoordinate2D coord = lastLocation.coordinate;
    NSLog(@"经度:%f,维度:%f",coord.longitude,coord.latitude);

维度值得是赤道以北或者以南多少度,其中赤道为0度,北极为90度,而南极为-90度。经度指的是本初子午线以东或者以西多少度,其中本初子午线是一条虚构的线条,他从北极触发,经过英国格林尼治天文台到达南极。位于本初子午线以西时,经度为负,最高可以达到-180度,而本初子午线以东,经度为正,最高可以达到180度。

作为经度和维度的坐标的补充,还有水平的精度,它用CLLocationDistance或者是米数表示。水平精度指的是实际位置与返回的坐标之间的距离在指定米数内。

    //水平精度
    CLLocationAccuracy horizontalAccuracy = lastLocation.horizontalAccuracy;
    NSLog(@"水平精度:%f",horizontalAccuracy);

CLLocation还提供了当前位置的海拔和垂直精度(单位是米)–如果设备装有GPS。如果设备没有装GPS,返回的海拔将为0;而垂直精度将为-1;

    //海拔
    CLLocationDistance altitude = lastLocation.altitude;
    NSLog(@"海拔高度:%f米",altitude);

    //垂直精度
    CLLocationAccuracy verticalAccuracy = lastLocation.verticalAccuracy;
    NSLog(@"垂直精度:%f米",verticalAccuracy);

CLLocation包含时间戳,它支出了这个位置是在位置管理器什么时候确定的。这可用于判断位置是否过期(进而忽略它),还可用于两个不同位置的时间。

    //时间戳
    NSDate *timestamp = lastLocation.timestamp;
    NSLog(@"%@",timestamp);

最后CLLocationHIA提供了速度(米每秒)和航向(相对于正北多少度)

    //速度
    CLLocationSpeed speed = lastLocation.speed;
    NSLog(@"你的速度是%f米每秒",speed);

    //航向
    CLLocationDirection direction = lastLocation.course;
    NSLog(@"航向是%f",direction);

重大变化通知

Apple强烈建议应用在获取位置后停止位置更新,以延长电池的续航时间。如果应用不要求位置非常准确,可监视重大位置变化,这是一种高效的方式,即能让应用获悉设备的位置发生重大变化,有可以避免GPS和Wifi不断监视当前位置,从而极大地节省电量。

        //监听重大位置发生变化
        [self.locationManager startMonitoringSignificantLocationChanges];

通常在设备变化超哦过500米或者更换了链接的基站时候,将发来通知。另外,仅当最后一次通知是在5分钟之前时候,才会发送新的通知。位置更新时间被交给委托方法locationManager:didUpdateLocations进行处理。

使用GPX文件进行位置测试

测试基于位置的应用让人望而止步,需要对不方便的位置进行测试的时候尤其如此。所幸XCode使用GPX文件提供了强大的测试支持。GPX文件是GPS交换格式(Exchange Format)文档,他使用XML格式,可用于设备和GPS之间交换信息。在调试模式下,Xcode可以使用GPX文件定义的“航点”来设备iOS模拟器或者设备的当前位置。

使用XCode 的EditScheme中的option选项,可以选择一个默认的模拟器位置。

地理编码和反编码

地理编码指的是找出认了能够看得懂的地址对应的经纬度;反编码值得是根据坐标做出人类可以看得懂的地址。在iOS5.0以后,CoreLocation就可以支持这两种功能了,而且不想以前那样存在特殊条款或者限制。

CLGeocoder最主要的两个方法就是- (void)geocodeAddressString:(NSString *)addressString completionHandler:(CLGeocodeCompletionHandler)completionHandler;- (void)reverseGeocodeLocation:(CLLocation *)location completionHandler:(CLGeocodeCompletionHandler)completionHandler;,分别用于地理编码和逆地理编码。


#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);
    }];
}

显示地图

MapKit框架为iOS提供了地图用户界面的功能,其中的基本类是MKMapView,它用来显示地图,处理用户与地图之间的交互以及管理标注(大头针)和覆盖层(线路图或者突出区域)。要更加深入了解iOS中的地图的工作原理,必须明白坐标系。

理解坐标系

在MapKit中,有两个坐标系:地图坐标系和视图坐标系。地图使用墨卡托投影,将3D世界投影到2D坐标系。坐标可以使用经度和纬度指定。地图视图表示显示在屏幕上面的地图部分,它使用标准的UIKit视图坐标,并负责告诉在什么地方显示地图坐标指定的点。

配置和定制MKMapView

直接在ViewController上面包含一个地图视图,在地图上显示用户的位置,并允许用户滚动和缩放;

- (IBAction)mapTypeSelectionChanged:(UISegmentedControl *)sender {
    switch (sender.selectedSegmentIndex) {
        case 0:
        {
            [self.mapView setMapType:MKMapTypeStandard];
        }
            break;
        case 1:
        {
            [self.mapView setMapType:MKMapTypeSatellite];
        }
            break;
        case 2:
        {
            [self.mapView setMapType:MKMapTypeHybrid];
        }
            break;

        default:
            break;
    }
}

除了设置地图类型外,另一种常见的定制是设置地图显示区域。在ViewController中,有一个名为zooMapToFitAnnotation方法,它检查用户当前喜欢的地点,调整地图的大小和中心位置,以覆盖所有这些地点。这个方法首先设置默认的最大和最小坐标。

    CLLocationCoordinate2D maxCoordinate = CLLocationCoordinate2DMake(-90.0, -180.0);

    CLLocationCoordinate2D minCoordinate = CLLocationCoordinate2DMake(90.0, 180.0);

接来下,这个方法获得地图上的所有标注,并找出这些标注的最大和最小经纬度

    //找出标注的最大最小经纬度
    maxCoordinate.latitude = [[currentPlaces valueForKeyPath:@"@max.latitude"]doubleValue];
    minCoordinate.latitude = [[currentPlaces valueForKeyPath:@"@min.latitude"]doubleValue];

    maxCoordinate.longitude = [[currentPlaces valueForKeyPath:@"max.longitude"]doubleValue];
    minCoordinate.longitude = [[currentPlaces valueForKeyPath:@"min.longitude"]doubleValue];

然后,这个方法根据最大最小经纬度坐标计算中心坐标

    CLLocationCoordinate2D centerCoordinate;
    centerCoordinate.longitude = (maxCoordinate.longitude+minCoordinate.longitude)/2.0;
    centerCoordinate.latitude = (maxCoordinate.latitude+minCoordinate.latitude)/2.0;

接下来,这个方法计算显示所有需要标注的跨度(span)。计算每个方向的跨度,计算每个方向的跨度时候,都乘以放大系数1.2,以便在最偏远的标注和视图边缘之间流出一定的边距。

    //计算所有标注需要的跨度。
    MKCoordinateSpan span;
    span.longitudeDelta = (maxCoordinate.longitude-minCoordinate.longitude)*1.2;
    span.latitudeDelta = (maxCoordinate.latitude-minCoordinate.latitude)*1.2;

计算中心点和跨度之后,便可以创建一个地图区域,并使用他来显示地图视图的显示区域。

    //设置地图视图显示区域
    MKCoordinateRegion newRegion = MKCoordinateRegionMake(centerCoordinate, span);

    [self.mapView setRegion:newRegion animated:YES];

响应用户的交互

可以给MKMapView指定委托,以便用户与地图交互做出响应。用户与地图包括平移,缩放,拖拽注释以及用户轻按标注做出的响应。

用户平移或者缩放地图时候,讲调用委托方法:mapView:regionWillChangeAnimated:mapView:regionDidChangeAnimated:。在应用中,我们不许需要采取额外的措施来缩放地图和调整注释。然而,如果在应用上显示了大量信息或者显示的信息随着缩放的等级而异,就可以使用这些委托方法删除不可见的地图注释以及添加新出现的注释。在应用中,这个委托方法演示了如何获取新的地图区域,这可用来查询要在地图上显示的内容。

//当用户已经完成平移缩放地图时候调用此代理方法
-(void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated{
    MKCoordinateRegion newRegion = [mapView region];
    CLLocationCoordinate2D center = newRegion.center;
    MKCoordinateSpan span = newRegion.span;

    NSLog(@"新的地图中心:<%f/%f>,新的跨度:<%f/%f>",center.latitude,center.longitude,span.latitudeDelta,span.longitudeDelta);

}

地图注释和覆盖层

地图视图是一种可滚动视图,行为独特;以标准的方式在其中添加姿势图,姿势图不会随着地图视图滚动,而是径直的,其相对于地图视图框架的位置始终不变,对悬停按钮或者标签来说,这种特点也许不错,但在地图上标注点和细节至关重要。要标出地图视图中感兴趣的点或者区域,可以使用地图注释和覆盖层。地图滚动或者缩放的时候,注释和覆盖层在地图上的位置保持不变。地图注释是地图上某单个坐标定义的,而地图覆盖层可以是线段,多边形或者其他复杂的形状。MapKit将注释(覆盖层)同其他关联的视图区分开来。注释和覆盖层是数据,指定了相关联的视图是显示在地图的什么地方,这些数据被直接添加到地图视图当中。在需要显示注释或者覆盖层的时候,地图视图将请求相关联的视图,就像表视图根据需要为索引路径请求单元格一样。

添加注释

任何对象都可以用作地图视图中的注释,前提条件是他实现了协议MKAnnotation。Apple建议注释对象应该是轻量级的,因为对于添加的每个注释,地图视图都将包含一个指向他的引用;另外,如果注释太多,会影响地图的滚动和缩放性能。如果注释分厂简单,可以直接使用MKPointAnnotation类。在实例当中,ICFavoritePlace实现协议KAnnotation,他是NSManagedObject的子类,因此可以使用CoreData进行持久化。

在实现协议MKAnnotation,类必须实现属性coordinate,地图视图将使用这个属性确定将注释放在地图施恩么地方,属性Coordinate的获取方法返回一个CLLocationCoordinate2D。

未完待续。。。

你可能感兴趣的:(UI,iOS开发高级)