Reputation: 6841
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
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
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