在前一篇文章 iOS关于地图定位基础(一) 中我们主要总结了 iOS 里面利用原生 CoreLocation 框架实现基本定位功能和一些注意点,侧重点主要是iOS8+之后的定位授权与授权状态的使用。接下来本篇文章主要是讲解如何利用 CoreLocation 框架实现地理定位、区域监听、地理编码的具体实现。(PS:下文涉及我自定义的指南针Demo请去我的GitHub仓库查看源码https://github.com/IMLoser/HWCompass,谢谢大家支持。)
(一、定位实现&监听方向)那么我们先来看看这个代理方法:
// 通过位置管理者一旦定位到位置,就会一直调用这个代理方法
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations;
在这个方法中共有两个参数,一个是位置管理者,另一个就是保存有位置对象(CLLocation)的数组,这个数组中位置对象的存放顺序是按照时间排序的,那么最新的定位信息永远是数组最后一个元素。那么 CLLocation 对象又是什么呢?我们看看以下代码:
#pragma mark - CLLocationManagerDelegate
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations {
CLLocation * location = [locations lastObject];
/*
CLLocation 位置对象的重要属性:
coordinate 定位到的经纬度坐标
altitude 海拔
horizontalAccuracy 水平精确度
verticalAccuracy 垂直精确度
course 航向(取值0 ~ 359.9)
speed 速度
*/
}
光看干巴巴的属性来学习始终不够形象,下面我们来看个小案例 : 显示用户每次行走的方向和角度以及针对于上一次定位行走的距离,如 : 北偏东 30度 移动了12米。代码如下 :
{
// 记录上一次位置
CLLocation *_oldLocation;
}
#pragma mark - CLLocationManagerDelegate
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations {
CLLocation * location = [locations lastObject];
NSString * locationInfo = nil;
NSInteger direction = (NSInteger)location.course / 90;
// 获取方向
switch (direction) {
case 0:
locationInfo = @"北偏东";
break;
case 1:
locationInfo = @"东偏南";
break;
case 2:
locationInfo = @"南偏西";
break;
case 3:
locationInfo = @"西偏北";
break;
default:
break;
}
// 获取角度
NSInteger angle = (NSInteger)location.course % 90;
if (!angle) {
locationInfo = [NSString stringWithFormat:@"正%@", [locationInfo substringToIndex:1]];
} else {
locationInfo = [locationInfo stringByAppendingString:[NSString stringWithFormat:@"%zd度", angle]];
}
// 获取移动的距离
NSInteger distance = 0;
if (_oldLocation) {
distance = [location distanceFromLocation:_oldLocation];
}
_oldLocation = location;
// 拼接打印
locationInfo = [locationInfo stringByAppendingString:[NSString stringWithFormat:@"移动了%zd米", distance]];
NSLog(@"%@", locationInfo);
}
我们不仅可以获取用户的位置信息,也可以获取用户的方向信息。这里可以简单的制作一个指南针控件,废话不多讲,我们先来看看效果图:
必须提一下的是,想要实现这个效果模拟器就有些力不从心,所以在运行效果Demo的时候我选择了真机。。。核心代码如下:
#import <UIKit/UIKit.h>
@interface HWCompass : UIView
// 获得指南针
+ (instancetype)getCompass;
// 开始获取方向
- (void)startUpdateHeading;
@end
#import "HWCompass.h"
#import <CoreLocation/CoreLocation.h>
@interface HWCompass () <CLLocationManagerDelegate>
/** 指南针视图 */
@property (nonatomic, weak) UIImageView * compassView;
/** 定位管理者 */
@property (strong, nonatomic) CLLocationManager * clManager;
@end
@implementation HWCompass
#pragma mark - lazy
- (CLLocationManager *)clManager
{
if (!_clManager) {
_clManager = [[CLLocationManager alloc] init];
_clManager.delegate = self;
if ([[UIDevice currentDevice].systemVersion floatValue] >= 8.0) {
[_clManager requestAlwaysAuthorization];
}
if ([[UIDevice currentDevice].systemVersion floatValue] >= 9.0) {
[_clManager allowsBackgroundLocationUpdates];
}
}
return _clManager;
}
+ (instancetype)getCompass
{
HWCompass * compass = [[HWCompass alloc] init];
compass.backgroundColor = [UIColor clearColor];
return compass;
}
- (void)startUpdateHeading
{
[self.clManager startUpdatingHeading];
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[self initailSetting];
}
return self;
}
- (void)awakeFromNib {
[super awakeFromNib];
[self initailSetting];
}
- (void)initailSetting
{
UIImageView * compassView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"compass"]];
_compassView = compassView;
[self addSubview:compassView];
}
- (void)layoutSubviews
{
[super layoutSubviews];
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 长宽相等
CGRect tmpFrame = self.frame;
tmpFrame.size.height = tmpFrame.size.width;
self.frame = tmpFrame;
// 设置指南针视图
_compassView.frame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);
});
}
#pragma mark - CLLocationManagerDelegate
- (void)locationManager:(CLLocationManager *)manager didUpdateHeading:(CLHeading *)newHeading {
[UIView animateWithDuration:0.1 animations:^{
_compassView.transform = CGAffineTransformMakeRotation(M_PI * 2 - M_PI / 180 * newHeading.magneticHeading);
}];
}
@end
以上这个是我自定义的指南针控件的代码,下面是控制器中的调用操作。。。
#import "ViewController.h"
#import "HWCompass.h"
@interface ViewController ()
/**
* 指南针
*/
@property (nonatomic, weak) HWCompass * compass;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 设置背景颜色
self.view.backgroundColor = [UIColor blackColor];
// 创建指南针控件
HWCompass * compass = [HWCompass getCompass];
_compass = compass;
compass.frame = CGRectMake(0, 0, 200, 200);
compass.center = CGPointMake(self.view.bounds.size.width * 0.5, self.view.bounds.size.height * 0.5);
[self.view addSubview:compass];
}
// 点击启动指南针功能
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[_compass startUpdateHeading];
}
@end
(二、区域监听)接下来我们来聊聊利用CoreLocation 框架实现简单的区域监听。这里需要补充的是在制作指南针的时候其实是没有必要申请用户授权的,因为获取方向不会涉及到用户隐私问题。但是用到区域监听功能时和定位的用户授权则是一样的。用到的核心类还是定位管理者CLLocationManager,懒加载创建、设置代理、授权都和定位功能实现是一样的;但是开启区域监听的方法、调用的代理确有些不同,具体代码实现如下 :
#import "ViewController.h"
#import <CoreLocation/CoreLocation.h>
@interface ViewController () <CLLocationManagerDelegate>
/** 定位管理者 */
@property (strong, nonatomic) CLLocationManager * clManager;
@end
@implementation ViewController
- (CLLocationManager *)clManager {
if (!_clManager) {
_clManager = [[CLLocationManager alloc] init];
// 设置代理
_clManager.delegate = self;
// 获取授权
if ([[UIDevice currentDevice].systemVersion floatValue] >= 8.0) {
// 获取前后台授权
[_clManager requestAlwaysAuthorization];
}
}
return _clManager;
}
// 点击屏幕开启区域监听
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 要监听圆形区域A的中心点
CLLocationCoordinate2D centerA = CLLocationCoordinate2DMake(42.22, 121.11);
// 设定监控的区域A
CLCircularRegion * regionA = [[CLCircularRegion alloc] initWithCenter:centerA radius:1000 identifier:@"区域A"];
// 开始区域监听区域A
[self.clManager startMonitoringForRegion:regionA];
// 要监听圆形区域B的中心点
CLLocationCoordinate2D centerB = CLLocationCoordinate2DMake(22.22, 80.11);
// 设定监控的区域B
CLCircularRegion * regionB = [[CLCircularRegion alloc] initWithCenter:centerB radius:1000 identifier:@"区域B"];
// 开始区域监听区域B
[self.clManager startMonitoringForRegion:regionB];
}
#pragma mark - CLLocationManagerDelegate
// 已经进入到监控的区域
- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region
{
NSLog(@"进入区域%@", region.identifier);
}
// 已经离开监听的区域
- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region
{
NSLog(@"离开区域%@", region.identifier);
}
@end
当我们视图更改模拟器坐标时,对应代理方法会针对是否进入或离开某个区域进行调用,具体打印如下 :
这里还有一个知识点的补充,我们还可以监听是否进入区域的状态,调用CLLocationManager 的实例方法 :
// 开始区域监听区域A
// [self.clManager startMonitoringForRegion:regionA];
// 监听是否进入指定区域的状态(以上开启区域监听方法不调用亦可)
[self.clManager requestStateForRegion:regionA];
实现对应的监听区域状态代理方法 :
- (void)locationManager:(CLLocationManager *)manager
didDetermineState:(CLRegionState)state forRegion:(CLRegion *)region;
- (void)locationManager:(CLLocationManager *)manager
didEnterRegion:(CLRegion *)region;
- (void)locationManager:(CLLocationManager *)manager
didExitRegion:(CLRegion *)region;
具体代码实现如下 :
#import "ViewController.h"
#import <CoreLocation/CoreLocation.h>
@interface ViewController () <CLLocationManagerDelegate>
/** 定位管理者 */
@property (strong, nonatomic) CLLocationManager * clManager;
@end
@implementation ViewController
- (CLLocationManager *)clManager {
if (!_clManager) {
_clManager = [[CLLocationManager alloc] init];
// 设置代理
_clManager.delegate = self;
// 获取授权
if ([[UIDevice currentDevice].systemVersion floatValue] >= 8.0) {
// 获取前后台授权
[_clManager requestAlwaysAuthorization];
}
}
return _clManager;
}
// 点击屏幕开启区域监听
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 要监听圆形区域A的中心点
CLLocationCoordinate2D centerA = CLLocationCoordinate2DMake(42.22, 121.11);
// 设定监控的区域A
CLCircularRegion * regionA = [[CLCircularRegion alloc] initWithCenter:centerA radius:1000 identifier:@"区域A"];
// 开始区域监听区域A
// [self.clManager startMonitoringForRegion:regionA];
// 监听是否进入指定区域的状态(以上开启区域监听方法不调用亦可)
[self.clManager requestStateForRegion:regionA];
}
#pragma mark - CLLocationManagerDelegate
// 已经进入到监控的区域
- (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
{
switch (state) {
case 0:
NSLog(@"未知的state");
break;
case 1:
NSLog(@"进入区域state");
break;
case 2:
NSLog(@"离开区域state");
break;
default:
break;
}
}
@end
(三、地理编码&反编码)最后我们聊聊地理编码和反编码,用到的核心类是CoreLocation 框架中的CLGeocoder(编码器),所谓地理编码简单点讲就是把地名转换为坐标(经纬度),那相反的把地理左边转换为地名等等就叫做地理反编码了。此外还要接触一个新类CLPlacemark。我们先来看下案例的效果图 :
具体代码如下 :
#import "ViewController.h"
#import <CoreLocation/CoreLocation.h>
@interface ViewController ()
/** 地理编码器 */
@property (strong, nonatomic) CLGeocoder * geocoder;
@property (weak, nonatomic) IBOutlet UITextView *clInfoName;
@property (weak, nonatomic) IBOutlet UITextField *clLatitude;
@property (weak, nonatomic) IBOutlet UITextField *clLongitude;
@end
@implementation ViewController
#pragma mark - lazy
- (CLGeocoder *)geocoder {
if (!_geocoder) {
_geocoder = [[CLGeocoder alloc] init];
}
return _geocoder;
}
// 地理编码
- (IBAction)geoClick {
_clLatitude.text = @"查询中...";
_clLongitude.text = @"查询中...";
[self.geocoder geocodeAddressString:_clInfoName.text completionHandler:^(NSArray<CLPlacemark *> * _Nullable placemarks, NSError * _Nullable error) {
if (error) {
_clLatitude.text = @"未查到";
_clLongitude.text = @"未查到";
return;
}
CLPlacemark * firstPlacemark = placemarks.firstObject;
_clLatitude.text = [NSString stringWithFormat:@"%.2f", firstPlacemark.location.coordinate.latitude];
_clLongitude.text = [NSString stringWithFormat:@"%.2f", firstPlacemark.location.coordinate.longitude];
}];
}
// 地理反编码
- (IBAction)reverseGeoClick {
_clInfoName.text = @"查询中...";
CLLocation * location = [[CLLocation alloc] initWithLatitude:[_clLatitude.text doubleValue] longitude:[_clLongitude.text doubleValue]];
[self.geocoder reverseGeocodeLocation:location completionHandler:^(NSArray<CLPlacemark *> * _Nullable placemarks, NSError * _Nullable error) {
if (error) {
_clInfoName.text = @"未查询到";
return;
}
CLPlacemark * firstPlacemark = placemarks.firstObject;
_clInfoName.text = firstPlacemark.locality;
}];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self.view resignFirstResponder];
_clInfoName.text = @"";
_clLatitude.text = @"";
_clLongitude.text = @"";
}
@end