Swift:后台持续定位并上传位置信息

Tip

最近一个项目中需要用户打开app后,当app处于前台和后台时能一直持续定位,并每隔一段时间上传位置信息。
iOS 11 对持续定位权限管理加强了,不建议app使用持续定位功能,所以需要添加使用期间的权限设置。


准备

  • 在 info.plist 文件中添加位置持续请求权限,这里一定要写出你的 app 为什么要使用持续定位功能的理由,否则,app 审核将不会通过。


NSLocationAlwaysAndWhenInUseUsageDescription
使用定位的描述
NSLocationAlwaysUsageDescription
使用定位的描述
NSLocationWhenInUseUsageDescription
使用定位的描述
  • 选中 TARGETS -> Capabilities 设置 Background Modes:


    Swift:后台持续定位并上传位置信息_第1张图片
  • 在 iTunes Connect 对应的 app 版本【描述】中添加注意事项:
Swift:后台持续定位并上传位置信息_第2张图片

代码

注意使用定位时,一定要这样设置

 /// 首先请求总是访问权限
        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()
    
    /// 请求 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 = [.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("定位失败")
    }
}

你可能感兴趣的:(Swift:后台持续定位并上传位置信息)