个人觉得地理位置这部分的知识其实是相当有趣的。说到定位其实在大多数的社交软件中都有这样的一个功能,用户可以共享自己的位置并且查看其他用户的位置,从而更容易地结交一些附近或者在同一地区的朋友,又或者在地图上显示自己的当前位置,并且显示附近的餐厅或者咖啡厅。这些功能看起来是挺复杂,但在实际的开发过程中,iOS为我们封装了大部分的功能,让我们不需要学习太底层的知识,通过他提供的两个库就能轻松实现。接下来我会一步一步地讲述相关知识。
1、获取当前定位
iOS提供了一个叫作CoreLocation.framework的框架。使用他可以取到自己的定位信息(经纬度)。请参考下面代码片段:
01 | if ([CLLocationManager locationServicesEnabled]){ |
03 | CLLocationManager *manager = [[CLLocationManager alloc] init]; |
04 | manager.distanceFilter = kCLDistanceFilterNone; |
05 | manager.desiredAccuracy = kCLLocationAccuracyBest; |
06 | manager.delegate = self; |
07 | [manager startUpdatingLocation]; |
09 | - ( void )locationManager:(CLLocationManager *)manager |
10 | didUpdateToLocation:(CLLocation *)newLocation |
11 | fromLocation:(CLLocation *)oldLocation |
14 | [manager stopUpdatingLocation]; |
16 | - ( void )locationManager:(CLLocationManager *)manager |
17 | didFailWithError:(NSError *)error |
19 | [manager stopUpdatingLocation]; |
如上面代码所示CLLocationManager就是用于获取定位信息对象类,在实际应用中可以根据自己的需要来设置定位的更新频率以及定位准确度。其中代码中的distanceFilter表示更新位置的距离,假如超过设定值则进行定位更新,否则不更新。代码中的kCLDistanceFilterNone表示不设置距离过滤,即随时更新地理位置。desiredAccuracy属性表示取得定位的精度,kCLLocationAccuracyBest表示最精确,但也预示着需要消耗更多的时间和电量,所以应该根据需要设定。
那么CLLocationManager是通过什么方法来开启定位的呢?他是通过调用startUpdatingLocation开启定位功能,然后使用stopUpdatingLocation停止定位,其中定位信息是通过loctionManager:didUpdateToLocation:fromLocation;委托方法来通知委托对象的,因此委托对象必须实现CLLocationManagerDelegate委托。在返回定位信息委托方法中主要的两个参数是newLocation和oldLocation,newLocation表示最新定位,oldLocation表示上一次的定位信息。这两个都是CLLocation对象。以下是CLLocation的属性说明:
属性 | 描述 |
altitude | 海拔高度 |
coordinate | 经纬度 |
course | 行驶方向 |
horizontalAccuracy | 水平方向的精确度 |
Speed | 行驶速度 |
timestamp | 时间戳 |
verticalAccuracy | 垂直方向的精确度 |
2、获取地理位置信息
当你取到了一个经纬度信息时,也许还有这样的一个需求,那就是当前的经纬度所对应的地理位置信息是什么。那么这时候我们需要用到框架来为我们实现这一功能,那就是MapKit.framework。在这个框架中有一个叫MKReverseGeocoder的类可以帮助我们实现反向解析地理位置。请看一下代码:
01 | - ( void )locationManager:(CLLocationManager *)manager |
02 | didUpdateToLocation:(CLLocation *)newLocation |
03 | fromLocation:(CLLocation *)oldLocation |
05 | MKReverseGeocoder *geocoder = [[MKReverseGeocoder alloc] initWithCoordinate:newLocation.coordinate]; |
06 | geocoder.delegate = self; |
10 | - ( void )reverseGeocoder:(MKReverseGeocoder *)geocoder |
11 | didFindPlacemark:(MKPlacemark *)placemark |
13 | NSLog(@ "\n country:%@\n postalCode:%@\n ISOcountryCode:%@\n locality:%@\n subLocality:%@\n administrativeArea:%@\n subAdministrativeArea:%@\n thoroughfare:%@\n subThoroughfare:%@\n" , |
16 | placemark.ISOcountryCode, |
17 | placemark.administrativeArea, |
18 | placemark.subAdministrativeArea, |
20 | placemark.subLocality, |
21 | placemark.thoroughfare, |
22 | placemark.subThoroughfare); |
25 | - ( void )reverseGeocoder:(MKReverseGeocoder *)geocoder |
26 | didFailWithError:(NSError *)error |
28 | NSLog(@ "reverse geocoder fail!!" ); |
上面的代码是在获取到经纬度后,立刻进行反向地理位置解析的。其实MKReverseGeocoder用法也比较简单,通过经纬度初始化后直接调用start方法就可以实现反向解析了,然后等待返回,其返回是通过委托形式通知的。所以委托对象必须实现MKReverseGeocoderDelegate委托。解析成功后会返回一个MKPlacemark的对象里面包含了相关的地理位置信息(包括国家、地区、街道等)。
但从iOS5之后MKReverseGeocoder成为了不推荐使用的类。因此有一个新的类取代了他的作用,那就是CLGeocoder类,使用该类进行反向解析也非常容易,请看下面代码:
01 | CLGeocoder *geocoder=[[CLGeocoder alloc] init]; |
02 | [geocoder reverseGeocodeLocation:newLocation |
03 | completionHandler:^(NSArray *placemarks, |
06 | CLPlacemark *placemark=[placemarks objectAtIndex:0]; |
07 | NSLog(@ "name:%@\n country:%@\n postalCode:%@\n ISOcountryCode:%@\n ocean:%@\n inlandWater:%@\n locality:%@\n subLocality:%@\n administrativeArea:%@\n subAdministrativeArea:%@\n thoroughfare:%@\n subThoroughfare:%@\n" , |
11 | placemark.ISOcountryCode, |
13 | placemark.inlandWater, |
14 | placemark.administrativeArea, |
15 | placemark.subAdministrativeArea, |
17 | placemark.subLocality, |
18 | placemark.thoroughfare, |
19 | placemark.subThoroughfare); |
从代码来看,CLGeocoder类没有使用委托的形式通知返回状态,而是通过block的方式进行回调,而且MKReverseGeocoder委托只返回了一个地标位置,但是CLGeocoder则返回了一个包含多个地标位置的数组,但这个数组在通常状态下是只有一个元素,如果存在多个元素那证明了给定解析的经纬度被解析到多个不同的地标信息。如果解析错误或者调用cancel方法则此参数为nil。
3、地图显示
想更加形象地表现出位置信息靠文字的描述是远远不够的,因为使用地图来显示地理位置将会给用户带来全新的体验。在iOS里面已经将Google地图封装到SDK里面了,我们可以用很少的代码来实现很多在地图上的操作(如标记位置、绘画线路等)。下面的代码是生成一张地图并显示到界面上:
4 | MKMapView *mapView=[[MKMapView alloc] initWithFrame:CGRectMake(0.0, 0.0, 320.0, 460.0)]; |
5 | mapView.delegate = self; |
6 | [self.view addSubview:mapView]; |
这够简单吧,上面的设置地图委托对象是因为在下面要标记地理位置时需要用到的。那么如何把取到的经纬度信息显示到地图上呢?其实每个坐标信息在地图中显示后都对应一个MKAnnotationView,而MKAnnotationView又负责解析了一个实现MKAnnotation协议的数据对象。因此我们首先要做的事情就是把取到的经纬度转换为MKAnnotation协议对象。先定义一个实现MKAnnotation协议的类:
01 | @interface DemoAnnotation : NSObject<MKAnnotation> { |
02 | CLLocationCoordinate2D _coordinate; |
05 | -(id)initWithCoordinate:(CLLocationCoordinate2D)coordinate; |
09 | @implementation DemoAnnotation |
11 | @synthesize coordinate=_coordinate; |
13 | -(id)initWithCoordinate:(CLLocationCoordinate2D)coordinate{ |
14 | if (self = [super init]) { |
15 | _coordinate=coordinate; |
20 | -( void )setCoordinate:(CLLocationCoordinate2D)newCoordinate{ |
21 | _coordinate=newCoordinate; |
28 | -(NSString *)subtitle{ |
上面的类只是简单地保存了经纬度信息。记得注意的是MKAnnotation协议中的title和subtitle的作用,如果你在显示AnnotationView设置其canShowCallout属性为YES时,则当用户点击AnnotationView时会弹出一个Callout视图,用于显示title和subtitle,假如设置title为nil那么即使canShowCallout为YES也不会弹出Callout视图。
接下来要改写一下获取定位成功后的方法,等待获取定位成功后把经纬度设置到地图上显示。实现代码如下:
01 | - ( void )locationManager:(CLLocationManager *)manager |
02 | didUpdateToLocation:(CLLocation *)newLocation |
03 | fromLocation:(CLLocation *)oldLocation |
05 | DemoAnnotation *annotation = [[DemoAnnotation alloc] initWithCoordinate:newLocation.coordinate]; |
06 | [_mapView addAnnotation:annotation]; |
09 | MKReverseGeocoder *geocoder = [[MKReverseGeocoder alloc] initWithCoordinate:newLocation.coordinate]; |
10 | geocoder.delegate = self; |
上述代码中加粗部分的代码就是把经纬度信息封装到刚才定义好的对象中。然后通过addAnnotation方法传递给MapView;这里的_mapView是把之前viewDidLoad方法中的临时变量改变为类属性以达到跨方法引用的目的。经过上面步骤已经把我的位置引入到地图里面了,但现在还不会显示在地图上,因为还需要实现MapView中的协议,告诉MapView如何显示你Annotation。以下代码采用iOS中默认的大头针样式来显示位置。如下:
01 | - (MKAnnotationView *)mapView:(MKMapView *)mapView |
02 | viewForAnnotation:(id <MKAnnotation>)annotation |
04 | NSString *annotationViewId=@ "CurrentUserAnnotationView" ; |
05 | MKPinAnnotationView *annotationView = (MKPinAnnotationView *) |
06 | [mapView dequeueReusableAnnotationViewWithIdentifier:annotationViewId]; |
07 | if (annotationView==nil) |
09 | annotationView = [[[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:annotationViewId] autorelease]; |
10 | annotationView.canShowCallout = YES; |
12 | return annotationView; |
上面的协议方法就是MapView告诉你他需要显示哪个annotation,然后显示的样式由用户你自己决定,但必须要继承MKAnnotationView类。
到这里对于定位和地图的应用就告一段落了,当然关于
MKMapView
还有一些更加高级的特性,例如:动态编辑
Annotation
、绘画路线图等应用我在这里暂时不说了,等偶再研究透彻一点再给大家分享。