美文网首页
Swift:后台持续定位并上传位置信息

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

作者: 七夕猪 | 来源:发表于2017-09-28 10:17 被阅读968次

    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("定位失败")
        }
    }
    

    相关文章

      网友评论

          本文标题:Swift:后台持续定位并上传位置信息

          本文链接:https://www.haomeiwen.com/subject/bzkmextx.html