跟踪一定距离内的位置变化 iOS

Track changes in the location for a certain distance iOS

我有一个任务是在后台跟踪用户的位置,如果它的位置已经改变到超过 5 英里,那么我需要在服务器上更新这个数据。我知道您可以使用 startMonitoringSignificantLocationChanges 开始跟踪用户位置。我开始测试,使用 startMonitoringSignificantLocationChangesallowsBackgroundLocationUpdates = true 启动应用程序,然后从模拟器内存​​中删除应用程序,进入地图并启用 Free Way 模拟。一分钟我在服务器上获得了 8 个更新,对我来说太频繁了。我认为对我来说,最好的解决方案是询问我们希望从多远的地方接收更新。我读了一些关于这个的帖子,但没有一个没有解决我的问题。我还认为您可以保存以前的位置并将更改与新位置进行比较,但我认为这是个坏主意。告诉我,如何更好地解决这个问题?

class LocationManager: NSObject {

    private override init() {
        super.init()
    }

    static let shared = LocationManager()

    private let locationManager = CLLocationManager()

    weak var delegate: LocationManagerDelegate?

    // MARK: - Flags

    private var isCallDidStartGetLocation = false

    // MARK: - Measuring properties

    private var startTimestamp = 0.0

    // MARK: - Open data

    var currentLocation: CLLocation?

    // MARK: - Managers 

    private let locationDatabaseManager = LocationDatabaseManager()

    // MARK: - Values

    private let metersPerMile = 1609.34

    func start() {
        // measuring data
        startTimestamp = Date().currentTimestamp
        FirebasePerformanceManager.shared.getUserLocation(true)

        locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
        locationManager.activityType = .other
        locationManager.distanceFilter = 100 
        locationManager.delegate = self
        let status = CLLocationManager.authorizationStatus()

        switch status {
        case .authorizedAlways:
            locationManager.startUpdatingLocation()
        case .authorizedWhenInUse:
            locationManager.requestAlwaysAuthorization()
            locationManager.startUpdatingLocation()
        case .restricted, .notDetermined:
            locationManager.requestAlwaysAuthorization()
        case .denied:
            showNoPermissionsAlert()
        }
    }

    func logOut() {
        locationManager.stopUpdatingLocation()
        isCallDidStartGetLocation = false
    }

}

// MARK: - Alerts

extension LocationManager {

    private func showNoPermissionsAlert() {
        guard let topViewController = UIApplication.topViewController() else { return }
        let alertController = UIAlertController(title: "No permission",
                                                message: "In order to work, app needs your location", preferredStyle: .alert)
        let openSettings = UIAlertAction(title: "Open settings", style: .default, handler: {
            (action) -> Void in
            guard let URL = Foundation.URL(string: UIApplicationOpenSettingsURLString) else { return }
            UIApplication.shared.open(URL, options: [:], completionHandler: nil)

        })
        alertController.addAction(openSettings)
        topViewController.present(alertController, animated: true, completion: nil)
    }

}

// MARK: - CLLocationManager Delegate

extension LocationManager: CLLocationManagerDelegate {

    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        switch status {
        case .authorizedWhenInUse, .authorizedAlways:
            locationManager.startUpdatingLocation()
        default: break
        }

        delegate?.didChangeAuthorization?(manager: manager, didChangeAuthorization: status)
    }

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let lastLocation = locations.last else { return }
        let timeInterval = abs(lastLocation.timestamp.timeIntervalSinceNow)

        guard timeInterval < 60 else { return }

        currentLocation = lastLocation
        locationDatabaseManager.updateUserLocation(lastLocation)
        measureGetLocationTime()
        if !isCallDidStartGetLocation {
            isCallDidStartGetLocation = true
            delegate?.didStartGetLocation?()
        }
    }

}

// MARK: - Calculation

extension LocationManager {

    func calculateDistanceFromCurrentLocation(_ venueLocation: CLLocation) -> Double {
        guard let userLocation = locationManager.location else {
            return 0.0
        }
        let distance = userLocation.distance(from: venueLocation)
        let distanceMiles = distance / DistanceConvertor.metersPerMile //1609
        return distanceMiles.roundToPlaces(places: 1)
    }

}

// MARK: - Measuring functions

extension LocationManager {

    private func measureGetLocationTime() {
        FirebasePerformanceManager.shared.getUserLocation(false)
        let endTimestamp = Date().currentTimestamp
        let resultTimestamp = endTimestamp - startTimestamp
        BugfenderManager.getFirstUserLocation(resultTimestamp)
    }

}

根据Apple Docs

Apps can expect a notification as soon as the device moves 500 meters or more from its previous notification. It should not expect notifications more frequently than once every five minutes. If the device is able to retrieve data from the network, the location manager is much more likely to deliver notifications in a timely manner.

startMonitoringSignificantLocationChanges() 是最不准确的位置监控方式,并且无法配置它在手机信号塔转换时触发的频率。因此它可以在塔(城市)更密集的地区更频繁地触发。有关详细信息,请参阅 this thread

我更改了当前的 LocationManager 并为此案例创建了两个新的管理器。我测试了应用程序,在我的更改之后,结果如下:我开了 120-130 公里,两个路段在城市之间,应用程序花费了设备费用的 1%,对我们来说这是一个可以接受的结果。应用程序向服务器发送了 4 次用户位置更新请求,条件如下:上次更新位置后耗时 2 小时,并且之前位置与新位置之间的距离为 5 英里或更多。您可以在下面看到实现。

位置管理器

import Foundation
import CoreLocation

class LocationManager: NSObject {

    private override init() {
        super.init()
        manager.delegate = self
    }

    static let shared = LocationManager()

    private let manager = CLLocationManager()

    weak var delegate: LocationManagerDelegate?

    // MARK: - Enums


    enum DistanceValue: Int {
        case meters, miles
    }

    // MARK: - Flags

    private var isCallDidStartGetLocation = false

    // MARK: - Measuring properties

    private var startTimestamp = 0.0

    // MARK: - Open data

    var currentLocation: CLLocation?

    // MARK: - Managers 

    private let locationDatabaseManager = LocationDatabaseManager()

    // MARK: - Values

    private let metersPerMile = 1609.34

    func start() {
        // measuring data
        startTimestamp = Date().currentTimestamp
        FirebasePerformanceManager.shared.getUserLocation(true)

        manager.desiredAccuracy = kCLLocationAccuracyHundredMeters
        manager.activityType = .other
        manager.desiredAccuracy = 45
        manager.distanceFilter = 100

        let status = CLLocationManager.authorizationStatus()

        switch status {
        case .authorizedAlways:
            if UIApplication.shared.applicationState != .background {
                manager.startUpdatingLocation()
            }

            manager.startMonitoringSignificantLocationChanges()
            manager.allowsBackgroundLocationUpdates = true
        case .authorizedWhenInUse:
            manager.requestAlwaysAuthorization()
            manager.startUpdatingLocation()
        case .restricted, .notDetermined:
            manager.requestAlwaysAuthorization()
        case .denied:
            showNoPermissionsAlert()
        }
    }

    func logOut() {
        manager.stopUpdatingLocation()
        isCallDidStartGetLocation = false
    }

}

// MARK: - Mode managing

extension LocationManager {

    open func enterBackground() {
        manager.stopUpdatingLocation()
        manager.startMonitoringSignificantLocationChanges()
    }

    open func enterForeground() {
        manager.startUpdatingLocation()
    }

}

// MARK: - Alerts

extension LocationManager {

    private func showNoPermissionsAlert() {
        guard let topViewController = UIApplication.topViewController() else { return }
        let alertController = UIAlertController(title: "No permission",
                                                message: "In order to work, app needs your location", preferredStyle: .alert)
        let openSettings = UIAlertAction(title: "Open settings", style: .default, handler: {
            (action) -> Void in
            guard let URL = Foundation.URL(string: UIApplicationOpenSettingsURLString) else { return }
            UIApplication.shared.open(URL, options: [:], completionHandler: nil)

        })
        alertController.addAction(openSettings)
        topViewController.present(alertController, animated: true, completion: nil)
    }

}

// MARK: - CLLocationManager Delegate

extension LocationManager: CLLocationManagerDelegate {

    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        switch status {
        case .authorizedWhenInUse, .authorizedAlways:
            if UIApplication.shared.applicationState != .background {
                manager.startUpdatingLocation()
            }
        default: break
        }

        delegate?.didChangeAuthorization?(manager: manager, didChangeAuthorization: status)
    }

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let lastLocation = locations.last else { return }

        let applicationState = UIApplication.shared.applicationState

        switch applicationState {
        case .active, .inactive:
            activeAppGetLocation(lastLocation)
        case .background:
            backgroundAppGetLocation(lastLocation)
        }
    }

}

// MARK: - Gettings location functions

extension LocationManager {

    private func activeAppGetLocation(_ location: CLLocation) {
        let timeInterval = abs(location.timestamp.timeIntervalSinceNow)

        guard timeInterval < 60 else { return }

        currentLocation = location
        locationDatabaseManager.updateUserLocation(location, state: .active)
        if !isCallDidStartGetLocation {
            measureGetLocationTime()
            isCallDidStartGetLocation = true
            delegate?.didStartGetLocation?()
        }
    }

    private func backgroundAppGetLocation(_ location: CLLocation) {
        let locationBackgroundManager = LocationBackgroundManager()
        locationBackgroundManager.updateLocationInBackgroundIfNeeded(location)
    }

}

// MARK: - Calculation

extension LocationManager {

    func calculateDistanceBetweenLocations(_ firstLocation: CLLocation, secondLocation: CLLocation, valueType: DistanceValue) -> Double {
        let meters = firstLocation.distance(from: secondLocation)
        switch valueType {
        case .meters:
            return meters
        case .miles:
            let miles = meters / DistanceConvertor.metersPerMile
            return miles
        }
    }

    /// In miles
    func calculateDistanceFromCurrentLocation(_ venueLocation: CLLocation) -> Double {
        guard let userLocation = manager.location else {
            return 0.0
        }
        let distance = userLocation.distance(from: venueLocation)
        let distanceMiles = distance / DistanceConvertor.metersPerMile //1609
        return distanceMiles.roundToPlaces(places: 1)
    }

}

// MARK: - Measuring functions

extension LocationManager {

    private func measureGetLocationTime() {
        FirebasePerformanceManager.shared.getUserLocation(false)
        let endTimestamp = Date().currentTimestamp
        let resultTimestamp = endTimestamp - startTimestamp
        BugfenderManager.getFirstUserLocation(resultTimestamp)
    }

}

位置背景管理器

import Foundation
import CoreLocation
import SwiftDate

class LocationBackgroundManager {

    private var backgroundLocationUpdateTimestamp: Double {
        get {
            return UserDefaults.standard.double(forKey: "backgroundLocationUpdateTimestamp")
        }
        set {
            UserDefaults.standard.set(newValue, forKey: "backgroundLocationUpdateTimestamp")
            UserDefaults.standard.synchronize()
        }
    }

    // MARK: - Managers

    private lazy var locationStorageManager: LocationStorageManager = {
        let locationStorageManager = LocationStorageManager()
        return locationStorageManager
    }()

    open func updateLocationInBackgroundIfNeeded(_ location: CLLocation) {
        if backgroundLocationUpdateTimestamp != 0 {
            let currentLocationDate = location.timestamp

            let previousDate = Date(timeIntervalSince1970: backgroundLocationUpdateTimestamp)

            guard let hours = (currentLocationDate - previousDate).in(.hour) else { return }

            guard hours >= 2 else { return }

            if let previousLocationRealm = locationStorageManager.getCurrentUserPreviousLocation() {
                let previousLocation = CLLocation(latitude: previousLocationRealm.latitude, longitude: previousLocationRealm.longitude)
                let distance = LocationManager.shared.calculateDistanceBetweenLocations(location, secondLocation: previousLocation, valueType: .miles)
                guard distance >= 5 else { return }

                updateLocation(location)
            } else {
                updateLocation(location)
            }
        } else {
           updateLocation(location)
        }
    }

    private func updateLocation(_ location: CLLocation) {
        let locationDatabaseManager = LocationDatabaseManager()
        locationDatabaseManager.updateUserLocation(location, state: .background)
        backgroundLocationUpdateTimestamp = location.timestamp.currentTimestamp
        locationStorageManager.saveLocation(location)
    }

}

位置存储管理器

import Foundation
import CoreLocation
import RealmSwift

class LocationStorageManager {

    func saveLocation(_ location: CLLocation) {
        guard let currentUserID = RealmManager().getCurrentUser()?.id else { return }
        let altitude = location.altitude
        let latitude = location.coordinate.latitude
        let longitude = location.coordinate.longitude

        let locationRealm = LocationRealm(altitude: altitude, latitude: latitude, longitude: longitude, userID: currentUserID)

        do {
            let realm = try Realm()
            try realm.write {
                realm.add(locationRealm, update: true)
            }
        } catch {
            debugPrint(error)
            let funcName = #function
            let file = #file
            BugfenderManager.reportError(funcName, fileName: file, error: error)
        }
    }

    func getCurrentUserPreviousLocation() -> LocationRealm? {
        guard let currentUserID = RealmManager().getCurrentUser()?.id else { return nil }
        do {
            let realm = try Realm()
            let previousLocation = realm.objects(LocationRealm.self).filter("userID == %@", currentUserID).first
            return previousLocation
        } catch {
            debugPrint(error)
            let funcName = #function
            let file = #file
            BugfenderManager.reportError(funcName, fileName: file, error: error)
            return nil
        }
    }

}