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

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("定位失败")
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,500评论 25 707
  • 工作了两年多,一直有个“坏习惯”,就是将工作中遇到的一些问题、技巧或心得记在印象笔记里面,按理来说,作为一个...
    F森阅读 1,981评论 3 26
  • APP开发中经常需要使用到后台定位上传功能,在IOS中可通过CLLocationManage类来实现。其中后台定位...
    dacheng阅读 5,682评论 5 13
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,599评论 18 139
  • 桂花香,斜雨凉, 望断天涯愁断肠。 月光黄,夜未央, 幽笛呜咽独仿徨。
    L里予阅读 419评论 10 6