Alexander Khitev
Alexander Khitev

Reputation: 6841

Track changes in the location for a certain distance iOS

I have a task to track the user's location in the background afterwards, and if its location has changed to more than 5 miles, then I need to update this data on the server. I know that you can start tracking user locations using startMonitoringSignificantLocationChanges. I started testing, launched the application with startMonitoringSignificantLocationChanges and allowsBackgroundLocationUpdates = true, then removed the application from the simulator memory, went into Maps and enabled Free Way simulation. For a minute I got 8 updates on the server, for me it's too often. I think for me, the best solution was if we ask what distance we want to receive updates from. I read a few posts about this, but not one did not solve my problem. I also thought that you can save the previous location and compare the changes with the new location, but I think this is a bad idea. Tell me, how to solve this problem better?

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)
    }

}

Upvotes: 0

Views: 2907

Answers (2)

Alexander Khitev
Alexander Khitev

Reputation: 6841

I changed the current LocationManager and created two new managers for this case. I tested the application, after my changes and the results are as follows: I drove 120-130 km, two segments of the way were between cities, the application spent 1% of the device's charge, for us this is an acceptable result. The App sent 4 requests to the server with the update of the user's location, the conditions were as follows: after the previous update the location took 2 hours and the distance between the previous and the new location was 5 or more miles. You can see the implementation below.

LocationManager

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)
    }

}

LocationBackgroundManager

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)
    }

}

LocationStorageManager

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
        }
    }

}

Upvotes: 4

nomadoda
nomadoda

Reputation: 4942

According to 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() is the least accurate way to monitor location and there is no way to configure how often it's called as it's triggered in the event of a cell tower transition. Therefore it can trigger more often in areas with more densely located tower (cities). See this thread for more information.

Upvotes: 1

Related Questions