Tip
最近一个项目中需要用户打开app后,当app处于前台和后台时能一直持续定位,并每隔一段时间上传位置信息。
iOS 11 对持续定位权限管理加强了,不建议app使用持续定位功能,所以需要添加使用期间的权限设置。
准备
-
在 info.plist 文件中添加位置持续请求权限,这里一定要写出你的 app 为什么要使用持续定位功能的理由,否则,app 审核将不会通过。
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>使用定位的描述</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>使用定位的描述</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>使用定位的描述</string>
-
选中 TARGETS -> Capabilities 设置 Background Modes:
- 在 iTunes Connect 对应的 app 版本【描述】中添加注意事项:
代码
注意使用定位时,一定要这样设置
/// 首先请求总是访问权限
locationManager.requestAlwaysAuthorization()
/// 然后请求使用期间访问权限
locationManager.requestWhenInUseAuthorization()
/// 是否允许系统自动暂停位置更新服务,默认为 true
/// 一定要设置为 false,否则app后台时,系统20分钟左右会自动暂停定位服务
locationService.pausesLocationUpdatesAutomatically = false
创建一个单例,使用方式:
LocationStepsManager.shared.availableLocationService()
代码
import UIKit
import RealmSwift
//import HealthKit
import RxSwift
import CoreLocation
class LocationStepsManager: NSObject {
private override init() {
super.init()
}
public static let shared = LocationStepsManager()
// let healthKitStore = HKHealthStore()
/// 定位服务必须设置为全局变量
let locationManager = CLLocationManager()
fileprivate var locationSteps: [LocationStepCount] = []
fileprivate var currentLocation: CLLocationCoordinate2D? {
didSet {
startWork()
}
}
fileprivate var currentStepCount: Int = 0
var storeThread: Thread?
var uploadThread: Thread?
var storeTimer: Timer?
var uploadTimer: Timer?
fileprivate var isAllowWork: Bool = false
/// 时间间隔
fileprivate let storeTimeInterval: TimeInterval = 120.0
fileprivate let uploadTimeInterval: TimeInterval = 120.0
fileprivate let disposeBag = DisposeBag()
fileprivate let uploadEvent = PublishSubject<UploadLoAndStepReq>()
/// 请求 HealthKit
/*
func availableHealthKit() {
guard HKHealthStore.isHealthDataAvailable() else {
log.error("This app requires a device that supports HealthKit")
return
}
guard let stepCount = HKObjectType.quantityType(forIdentifier: .stepCount) else {
log.error("stepCount error")
return
}
let status = healthKitStore.authorizationStatus(for: stepCount)
switch status {
case .sharingDenied:
showMessageAlert(
title: "提示",
message: "智慧老山无法获取到您的步数信息,请到[设置]->[隐私]->[健康]->[智慧老山]中允许访问您的步数信息"
)
default:
healthKitStore.requestAuthorization(toShare: nil, read: [stepCount]) { [unowned self] (success, error) in
if success {
self.startLocation()
} else {
log.error("获取步数权限失败!")
}
}
}
}*/
/// 请求后台持续GPS定位服务
func availableLocationService() {
let locationServicesEnabled = CLLocationManager.locationServicesEnabled()
guard locationServicesEnabled else {
showMessageAlert(
title: "您的定位服务已关闭",
message: "请到[设置]->[隐私]中打开[定位服务]为了景区管理人员掌握你在景区的实时位置和行走轨迹,以便于在紧急情况下实施救援和道路指南;在景区内记录你的历史游玩记录的轨迹信息。"
)
return
}
let status = CLLocationManager.authorizationStatus()
switch status {
case .authorizedAlways, .notDetermined, .authorizedWhenInUse:
startLocation()
default:
showSettingAlert(
title: "智慧老山无法获得您的持续定位权限",
message: "为了景区管理人员掌握你在景区的实时位置和行走轨迹,以便于在紧急情况下实施救援和道路指南;在景区内记录你的历史游玩记录的轨迹信息。请设置[位置]始终允许访问您的位置信息。"
)
}
}
/// 开始定位
fileprivate func startLocation() {
locationManager.distanceFilter = 10
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
locationManager.delegate = self
/// 首先请求总是访问权限
locationManager.requestAlwaysAuthorization()
/// 然后请求使用期间访问权限
locationManager.requestWhenInUseAuthorization()
/// 是否允许系统自动暂停位置更新服务,默认为 true,设置为 false,否则会自动暂停定位服务,app 20分钟后就不会上传位置了
locationManager.pausesLocationUpdatesAutomatically = false
if #available(iOS 9.0, *) {
// 如果APP处于后台,则会出现蓝条
locationManager.allowsBackgroundLocationUpdates = true
}
locationManager.startUpdatingLocation()
handleUploadEvent()
}
fileprivate func startWork() {
guard let coordinate = currentLocation else { return }
/// 如果此时的位置在范围之内,就启动线程;否则关闭线程。
guard isInRange(coordinate: coordinate) else {
if isAllowWork {
stopThread()
isAllowWork = false
}
return
}
if !isAllowWork {
isAllowWork = true
setupThread()
}
}
/// 建立子线程
fileprivate func setupThread() {
// 1.存储线程
storeThread = Thread(
target: self,
selector: #selector(LocationStepsManager.setupStoreTimer),
object: nil
)
storeThread?.start()
// 2.上传线程
uploadThread = Thread(
target: self,
selector: #selector(LocationStepsManager.setupUploadTimer),
object: nil
)
uploadThread?.start()
}
fileprivate func stopThread() {
storeTimer?.invalidate()
storeTimer = nil
uploadTimer?.invalidate()
uploadTimer = nil
storeThread?.cancel()
storeThread = nil
uploadThread?.cancel()
uploadThread = nil
}
/// 创建存储计时器
@objc fileprivate func setupStoreTimer() {
storeTimer?.invalidate()
let timer = Timer(
timeInterval: storeTimeInterval,
target: self,
selector: #selector(LocationStepsManager.storeLoactionStepCount),
userInfo: nil,
repeats: true
)
RunLoop.current.add(timer, forMode: .defaultRunLoopMode)
RunLoop.current.run()
storeTimer = timer
}
/// 创建上传计时器
@objc fileprivate func setupUploadTimer() {
uploadTimer?.invalidate()
let timer = Timer(
timeInterval: uploadTimeInterval,
target: self,
selector: #selector(LocationStepsManager.uploadLoctionStepCount),
userInfo: nil,
repeats: true
)
RunLoop.current.add(timer, forMode: .defaultRunLoopMode)
RunLoop.current.run()
uploadTimer = timer
}
/// 获取步数
/*
@objc fileprivate func getStepCount() {
let now = Date()
let calendar = Calendar.current
let components: Set<Calendar.Component> = [.year, .month, .day, .hour, .minute, .second]
let dateComponents = calendar.dateComponents(components, from: now)
let hour = dateComponents.hour!
let minute = dateComponents.minute!
let second = dateComponents.second!
let nowDay = Date.init(timeIntervalSinceNow: -(Double(hour * 3600 + minute * 60 + second)))
let nextDay = Date.init(timeIntervalSinceNow: -(Double(hour * 3600 + minute * 60 + second)) + 86400)
let mostRecentPredicate = HKQuery.predicateForSamples(withStart: nowDay, end: nextDay)
let starSortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false)
let endSortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
guard let stepCountType = HKQuantityType.quantityType(forIdentifier: .stepCount) else {
fatalError("stepCount error")
}
let query = HKSampleQuery(sampleType: stepCountType, predicate: mostRecentPredicate, limit: 0, sortDescriptors: [starSortDescriptor, endSortDescriptor]) { [unowned self] (query, results, error) in
if let queryError = error {
log.error(queryError.localizedDescription)
} else {
guard let resultArray = results else { return }
self.currentStepCount = 0
for item in resultArray {
let quantitySample = item as! HKQuantitySample
let quantity = quantitySample.quantity
let stepCount = Int(quantity.doubleValue(for: HKUnit.count()))
self.currentStepCount += stepCount
}
self.storeLoactionStepCount()
}
}
healthKitStore.execute(query)
}*/
@objc fileprivate func storeLoactionStepCount() {
guard let currentCoordinate = currentLocation else { return }
guard isInRange(coordinate: currentCoordinate) else { return }
log.debug("TotalStepCount:\(currentStepCount)")
let dateString = Date().format(style: .style6)
let model = LocationStepCount(
date: dateString,
steps: "\(currentStepCount)",
latitude: currentCoordinate.latitude,
longitude: currentCoordinate.longitude
)
locationSteps.append(model)
}
/// 上传位置及步数
@objc fileprivate func uploadLoctionStepCount() {
guard let currentCoordinate = currentLocation else { return }
if isInRange(coordinate: currentCoordinate) {
if locationSteps.count == 0 {
storeLoactionStepCount()
} else {
let lastStep = locationSteps.last!
if (currentCoordinate.longitude != lastStep.longitude) || (currentCoordinate.latitude != lastStep.latitude) {
storeLoactionStepCount()
}
}
}
guard locationSteps.count > 0 else { return }
let request = UploadLoAndStepReq(vipId: config.vipId, list: locationSteps)
self.uploadEvent.onNext(request)
}
deinit {
log.verbose("deinit")
locationManager.delegate = nil
}
}
// MARK: - Handle event
extension LocationStepsManager {
fileprivate func handleUploadEvent() {
uploadEvent
.flatMapLatest {
HttpAPI.shared.uploadLoactionSteps(request: $0).toAny()
}
.delay(0.3, scheduler: MainScheduler.instance)
.subscribe { [unowned self] in
$0.flatMapError {
log.error($0.localizedDescription)
}
$0.flatMap({ (res: ResponseBase) in
guard res.resultInteger > 0 else { return }
self.locationSteps.removeAll()
log.info("upload location and stpes success!")
})
}
.disposed(by: disposeBag)
}
}
// MARK: - CLLocationManagerDelegate
extension LocationStepsManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// WGS-84 坐标,GPS 原生数据
guard let coor = locations.last?.coordinate else { return }
log.debug("WGS84:\(coor)")
// 转换为百度坐标
// 转换WGS84坐标至百度坐标(加密后的坐标)
let dic = BMKConvertBaiduCoorFrom(coor, BMK_COORDTYPE_GPS)
let baiduCoor = BMKCoorDictionaryDecode(dic)
log.debug("BD09LL:\(baiduCoor)")
currentLocation = baiduCoor
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
log.error("定位失败")
}
}