最近一个项目中需要用户打开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:
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