前言: 本章會使用OC和Swift分別進行實現,需要瞭解Swift的小伙伴可以翻一下之前的博文 LBS和SoloMo(索羅門) LBS:基於位置的服務,根據定位展示周邊美食、景點等信息(全稱:Location Bassed Service) SoloMo:將位置社交、本地、移動化(全稱:Soclal ...
前言:
本章會使用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進入後臺處理,耗電量小
- 劣勢:定位精度相對於標准定位服務較低,更新的頻率根據當前位置附近的基站密度決定
後臺繼續定位
如果想要在後臺繼續進行定位,需要打開後臺的定位模式
拓展:
- 標準的定位服務(基於GPS/Wifi/基站的定位服務)
- 程式被完全關閉後就無法再獲取位置信息
- 顯著位置變化定位服務(基於基站的定位服務,設備必須有電話模塊支持)
- 當APP被完全關閉後,也可以接收到位置通知,並且讓APP進入後臺處理
- 定位精度相對於標准定位服務較低,耗電量小,更新的頻率根據當前位置附近的基站密度決定
iOS8之後定位
- 從iOS8開始,蘋果進一步加強了對用戶隱私的保護,當APP想範圍用戶隱私信息的時候,系統不再自動彈出對話框讓用戶授權,為了能讓系統自動彈出用戶授權界面,需要進行下麵設置
- 解決方案:調用iOS8的API,主動請求用戶授權
// 註意:根據官方文檔的解釋,在使用下麵2個方法的時候,如果不在info.plist中配置NSLocationWhenInUseUsageDescription這個key,那麼方法都不會生效 // 請求前臺定位授權 - (void)requestWhenInUseAuthorization // 註意:根據官方文檔的解釋,在使用下麵2個方法的時候,如果不在info.plist中配置`NSLocationAlwaysUsageDescription`這個key,那麼方法都不會生效 // 請求前後臺定位授權 - (void)requestAlwaysAuthorization
- 解決方案:調用iOS8的API,主動請求用戶授權
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之前,需要將開啟授權的截圖展示給用戶,讓用戶根據截圖去開啟授權
- 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)")
}
}
先到這,最近太忙,過兩天找個時間根據定位做個小項目再分享出來