Track changes in the location for a certain distance iOS - 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)
}
}

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

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.

Related

Timer is not allowing to add annotation on mapView in iOS swift

I have a situation where I have to call an API to fetch some Vehicles Locations objects in an array after getting the user current location. After fetching vehicles, I have to get the address also from Vehicles Locations data, so for 'n' Vehicles, there will be an 'n' API call and then add annotations on Map.
After that, I have to refresh the Vehicles data every 1 min. So, I created a timer but even after getting the API response, annotations are not displaying on map. Kindly look into this issue.
Below is Map View
import MapKit
class MapViewController: UIViewController, MKMapViewDelegate {
#IBOutlet private var mapView: MKMapView!
var currentLocation: CLLocation?
var user: User?
lazy var vehicleViewModel = {
VehicleViewModel()
}()
var locationUpdateTimer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
configureLocationManager()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
stopTimer()
}
func configureLocationManager() {
LocationManager.shared().delegate = self
LocationManager.shared().initializeLocationManager()
}
func configureTimer() {
if locationUpdateTimer == nil {
locationUpdateTimer = Timer.scheduledTimer(timeInterval: 60, target: self, selector: #selector(runLocationTimer), userInfo: nil, repeats: true)
}
}
#objc func runLocationTimer() {
fetchVehiclesLocation()
}
func resetMap() {
let annotations = mapView.annotations
mapView.removeAnnotations(annotations)
mapView = nil
}
func initializeMapView() {
mapView = MKMapView(frame: CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: self.view.frame.size.height))
mapView.delegate = self
}
func configureMapView() {
let mapDetail = vehicleViewModel.getLatitudeLongitudeLatitudeDeltaLongitudeDelta()
if let latitude = mapDetail.0, let longitude = mapDetail.1, let latitudeDelta = mapDetail.2, let longitudeDelta = mapDetail.3 {
let region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: latitude, longitude: longitude), latitudinalMeters: latitudeDelta, longitudinalMeters: longitudeDelta)
let scaledRegion: MKCoordinateRegion = mapView.regionThatFits(region)
mapView.setRegion(scaledRegion, animated: true)
mapView.setCameraBoundary(
MKMapView.CameraBoundary(coordinateRegion: region),
animated: true)
let zoomRange = MKMapView.CameraZoomRange(maxCenterCoordinateDistance: 100000)
mapView.setCameraZoomRange(zoomRange, animated: true)
mapView.register(
VehicleAnnotationView.self,
forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
}
}
func fetchVehiclesLocation() {
configureTimer()
initViewModel {
DispatchQueue.main.async {
self.resetMap()
self.initializeMapView()
self.configureMapView()
}
if let user = self.user {
self.vehicleViewModel.fetchVehicleAddress(user: user, completion: { status in
if self.vehicleViewModel.vehicleAnnotationItems.count == 0 {
self.alertWithTitleAndMessageWithOK(("Alert" , "error while fetching vehicle locations"))
} else {
DispatchQueue.main.async {
self.mapView.addAnnotations(self.vehicleViewModel.vehicleAnnotationItems)
}
}
})
}
}
}
func initViewModel(completion: #escaping () -> Void) {
if let user = self.user, let userId = user.userId {
vehicleViewModel.getVehiclesLocation(userId: userId) { (vehicleApiResponse, error) in
if vehicleApiResponse != nil {
completion()
} else {
self.alertWithTitleAndMessageWithOK(("Alert" , error?.localizedDescription ?? "error while fetching vehicles"))
}
}
}
}
func stopTimer() {
if locationUpdateTimer != nil {
locationUpdateTimer!.invalidate()
locationUpdateTimer = nil
}
}
deinit {
stopTimer()
}
}
//MARK: - LocationManagerDelegate methods
extension MapViewController: LocationManagerDelegate {
func didFindCurrentLocation(_ location: CLLocation) {
currentLocation = location
if let currentLocation = currentLocation, (currentLocation.horizontalAccuracy >= 0) {
mapView.showsUserLocation = true
fetchVehiclesLocation()
}
}
}
LocationManager Extension class
import CoreLocation
protocol LocationManagerDelegate: AnyObject {
func didFindCurrentLocation(_ location: CLLocation)
func didFailedToFindCurrentLocationWithError(_ error: NSError?)
func alertLocationAccessNeeded()
}
/**
This class acts as a Singleton for getting location manager updates across the application.
*/
class LocationManager: NSObject {
var manager: CLLocationManager!
private static var sharedNetworkManager: LocationManager = {
let networkManager = LocationManager()
return networkManager
}()
private override init() {
super.init()
manager = CLLocationManager()
}
class func shared() -> LocationManager {
return sharedNetworkManager
}
weak var delegate: LocationManagerDelegate?
//Entry point to Location Manager. First the initialization has to be done
func initializeLocationManager() {
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.distanceFilter = kCLDistanceFilterNone
manager.delegate = self
manager.requestWhenInUseAuthorization()
manager.allowsBackgroundLocationUpdates = false
startUpdating()
}
//Start updating locations
func startUpdating() {
manager.startUpdatingLocation()
}
//Check for whether location services are disabled.
func locationServicesEnabled() -> Bool {
let isAllowed = CLLocationManager.locationServicesEnabled()
return isAllowed
}
}
//MARK: - CLLocation Manager delegate methods
extension LocationManager: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else {
return
}
manager.stopUpdatingLocation()
delegate?.didFindCurrentLocation(location)
// manager.delegate = nil
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
delegate?.didFailedToFindCurrentLocationWithError(error as NSError?)
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
switch manager.authorizationStatus {
case .notDetermined:
self.manager.requestWhenInUseAuthorization()
break
case .authorizedWhenInUse, .authorizedAlways:
if locationServicesEnabled() {
self.startUpdating()
}
case .restricted, .denied:
delegate?.alertLocationAccessNeeded()
#unknown default:
print("Didn't request permission for location access")
}
}
}
Your code has a number of problems.
Neither your initializeMapView() function nor your resetMap() function make any sense.
You should add an MKMapView to your Storyboard, then connect it to your mapView outlet, and then don't assign a new value to mapView. Don't set it to nil, and don't replace the map view in the outlet with a brand new map view you create (like you're doing in initializeMapView().) Both of those things will prevent your map from displaying.
You also never create a timer except in your fetchVehiclesLocation() function, which doesn't seem right.
You also don't show how you're setting up your location manager and asking for location updates. (You call a function initializeLocationManager(). I don't believe that is an Apple-provided function. I'm guessing you added it in an extension to the location manager, but you don't show that code.)
You need to ask if the user has granted permission to use the location manager, and trigger a request for permission if not.
Once you have permission to use the location manager, you need to ask it to start updating the user's location.
You don't show any of that code.
Maybe you're doing that in code you didn't show? It also looks like you don't have your CLLocationManagerDelegate methods defined correctly. I don't know of any delegate method didFindCurrentLocation(_:). You will likely need to implement one of the delegate methods locationManager(_:didUpdateLocations:) or locationManager(_:didUpdateTo:from:).
I suggest searching for a tutorial on using the location manager to display the user's location on a map. It's a little involved, and requires some study to set up.

Cannot assign locationManager() function to DispatchQueue.main.async { * }

I am getting an error while trying to assign function locationManager() in the DispatchQueue.main.async {}, ill provide whole code and specific pic of error for more clarity --> Here
I got most of the code from SeanAllen on yt since I am new to swift and learning everyday so this code isn't my logic, and the function fetchAndReloadData() is my functionalists I created to get the lat and long from the API assigning to the correct car id since it will track Vehicles on map (car tracking app)
class MapViewController: UIViewController, MKMapViewDelegate {
var globalVechicle = [Vehicles]()
var id = "6438367CC43848B497FE4604AF465D6A"
let locationManager = CLLocationManager()
let regionInMeters: Double = 10000
#IBOutlet weak var mapView: MKMapView!
#IBAction func changeMapType(_ sender: UISegmentedControl) {
if sender.selectedSegmentIndex == 0 {
mapView.mapType = .standard
}else {
mapView.mapType = .satellite
}
}
#IBAction func closeButton(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
checkLocationServices()
}
func setupLocationManager() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
}
func centerViewOnUserLocation() {
if let location = locationManager.location?.coordinate {
let region = MKCoordinateRegion.init(center: location, latitudinalMeters: regionInMeters, longitudinalMeters: regionInMeters)
mapView.setRegion(region, animated: true)
}
}
func checkLocationServices() {
if CLLocationManager.locationServicesEnabled() {
setupLocationManager()
checkLocationAuthorization()
} else {
// show alert they have to turn it on
}
}
func checkLocationAuthorization() {
switch CLLocationManager.authorizationStatus() {
case .authorizedWhenInUse:
mapView.showsUserLocation = true
centerViewOnUserLocation()
fetchAndReloadData()
locationManager.startUpdatingLocation()
break
case .denied:
// show alert instructing them how to turn on the permissions
break
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
case .restricted:
// Show an alert letting them know what's up
break
case .authorizedAlways:
break
#unknown default:
print("Error")
}
}
}
extension MapViewController: CLLocationManagerDelegate {
func fetchAndReloadData(){
APICaller.shared.getVehicles(for: id) {[weak self] (result) in
guard let self = self else { return }
switch result {
case .success(let vehicle):
self.globalVechicle = vehicle
DispatchQueue.main.async {
self.locationManager()
}
case .failure(let error):
print(error)
}
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let lattitude = globalVechicle[0].Latitude ,let longitude = globalVechicle[0].Longitude else { return }
let carLocation = CLLocationCoordinate2D(latitude: lattitude , longitude: longitude)
let region = MKCoordinateRegion.init(center: carLocation, latitudinalMeters: regionInMeters, longitudinalMeters: regionInMeters)
mapView.setRegion(region, animated: true)
mapView.delegate = self
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
checkLocationAuthorization()
}
}

CLLocationManager suddenly only returning New York, NY on Device?

This code has always worked reliably but lately (at least on my Watch) it's always returning New York, New York no matter where I am? Did something change in Core Location? 🤔
import CoreLocation
class WorkoutLocationManager: NSObject, CLLocationManagerDelegate {
private var locationManager: CLLocationManager?
public var formattedWorkoutAddress: String?
public func getWorkoutLocation() {
guard CLLocationManager.locationServicesEnabled() else {
print("User does not have location services enabled")
return
}
locationManager = CLLocationManager()
locationManager?.delegate = self
locationManager?.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
let locationAuthorizationStatus = CLLocationManager.authorizationStatus()
switch locationAuthorizationStatus {
case .authorizedAlways:
print("location authorized Always")
locationManager?.requestLocation()
case .authorizedWhenInUse:
print("location authorized When in Use")
locationManager?.requestLocation()
case .denied:
print("location authorization denied")
case .notDetermined:
print("location authorization not determined")
case .restricted:
print("location authorization restricted")
default: ()
}
}
// MARK: - CLLocationManagerDelegate
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let currentLocation = locations.first else { return }
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(currentLocation) { (placemarksArray, error) in
if let unwrappedError = error {
print("Geocoder error: \(unwrappedError)")
}
guard let placemarksArrayUnwrapped = placemarksArray else { return }
if placemarksArrayUnwrapped.count > 0 {
if let placemark = placemarksArray?.first {
let locality = placemark.locality ?? ""
let state = placemark.administrativeArea ?? ""
let workoutLocationAsString = (locality + " " + state)
print("workoutLocationAsString = \(workoutLocationAsString)")
self.formattedWorkoutAddress = workoutLocationAsString
} else { print("no placemark")}
} else { print("placemark.count = 0")}
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("location manager error = \(error)")
}
//I added this code below to prevent getting the error "Failure to deallocate CLLocationManager on the same runloop as its creation may result in a crash" code is from this answer: https://stackoverflow.com/questions/52304969/failure-to-deallocate-cllocationmanager-on-the-same-runloop-as-its-creation-may?noredirect=1#comment95470009_52304969
override init() {
super.init()
self.performSelector(onMainThread: #selector(initLocationManager), with: nil, waitUntilDone: true)
}
#objc private func initLocationManager() {
self.locationManager = CLLocationManager()
self.locationManager?.delegate = self
}
#objc private func deinitLocationManager() {
locationManager = nil
}
deinit {
self.performSelector(onMainThread: #selector(deinitLocationManager), with: nil, waitUntilDone: true)
}
}
I finally figured it out, I did have a default location of NY/NY set in my Scheme...I must have done it a long time ago and forgot 🤦‍♂️

Retrieve current location when application is in background

I've built an application where you can press a start button. Once the button is pressed the application will get user location every 10 second all the way till the stop button is pressed. When I leave the application or if the screen gets black, it will NOT get any more locations till I re-enter the application.
So, I'm currently trying to update the locations when the application is minimized. (I guess it's called in the background?), and also when the screen turns black. But my questions is:
Should I write this code in the AppDelegate?, if so. How can I know
if the button was pressed or not?
Where exactly in the AppDelegate should I add the code? And how can
I pass the locations back to the correct ViewController? (Since I
cannot make any prepare for segue from AppDelegate)
If you know the answers of this questions, please do not hesitate to answer them. :) I would really appreciate it!
The best way to get user's location in background is to use the Significant-Change Location Service according to apple's documentation put this func in your class:
func startReceivingSignificantLocationChanges() {
let authorizationStatus = CLLocationManager.authorizationStatus()
if authorizationStatus != .authorizedAlways {
// User has not authorized access to location information.
return
}
if !CLLocationManager.significantLocationChangeMonitoringAvailable() {
// The service is not available.
return
}
locationManager.delegate = self
locationManager.startMonitoringSignificantLocationChanges()
}
and also this func:
func locationManager(_ manager: CLLocationManager, didUpdateLocations
locations: [CLLocation]) {
let lastLocation = locations.last!
// Do something with the location.
}
so you just need to call startReceivingSignificantLocationChanges() inside your button and it will call locationManager(_ manager: CLLocationManager,didUpdateLocations locations: [CLLocation]), so do what you want with the location there.
Remember to ask permission to use location and to stop tracking with locationManager.stopMonitoringSignificantLocationChanges()
Take location permission for Always Allow
Set location manager for allowsBackgroundLocationUpdates true
from the above way you can get location in every location changes store this information and it send to server. Below is the sample code
typealias LocateMeCallback = (_ location: CLLocation?) -> Void
/*
LocationTracker to track the user in while navigating from one place to other and store new locations in locations array.
**/
class LocationTracker: NSObject {
static let shared = LocationTracker()
var lastLocation: CLLocation?
var locations: [CLLocation] = []
var previousLocation: CLLocation?
var isPreviousIsSameAsCurrent: Bool {
if let previous = previousLocation, let last = lastLocation {
return previous == last
}
return false
}
var isAggressiveModeOn = false
var locationManager: CLLocationManager = {
let locationManager = CLLocationManager()
locationManager.allowsBackgroundLocationUpdates = true
locationManager.pausesLocationUpdatesAutomatically = true
locationManager.activityType = .automotiveNavigation
return locationManager
}()
var locateMeCallback: LocateMeCallback?
var isCurrentLocationAvailable: Bool {
if lastLocation != nil {
return true
}
return false
}
func enableLocationServices() {
locationManager.delegate = self
switch CLLocationManager.authorizationStatus() {
case .notDetermined:
// Request when-in-use authorization initially
locationManager.requestWhenInUseAuthorization()
case .restricted, .denied:
// Disable location features
print("Fail permission to get current location of user")
case .authorizedWhenInUse:
// Enable basic location features
enableMyWhenInUseFeatures()
case .authorizedAlways:
// Enable any of your app's location features
enableMyAlwaysFeatures()
}
}
func enableMyWhenInUseFeatures() {
locationManager.startUpdatingLocation()
locationManager.delegate = self
escalateLocationServiceAuthorization()
}
func escalateLocationServiceAuthorization() {
// Escalate only when the authorization is set to when-in-use
if CLLocationManager.authorizationStatus() == .authorizedWhenInUse {
locationManager.requestAlwaysAuthorization()
}
}
func enableMyAlwaysFeatures() {
enableCoarseLocationFetch()
locationManager.startUpdatingLocation()
locationManager.delegate = self
}
// Enable Rough Location Fetch
func enableCoarseLocationFetch() {
isAggressiveModeOn = false
locationManager.desiredAccuracy = kCLLocationAccuracyKilometer
locationManager.distanceFilter = 100
}
// Enable Aggressive Location Fetch
func enableAggressiveLocationFetch() {
isAggressiveModeOn = true
locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
locationManager.distanceFilter = 10
}
func locateMe(callback: #escaping LocateMeCallback) {
self.locateMeCallback = callback
if lastLocation == nil {
enableLocationServices()
} else {
callback(lastLocation)
}
}
func startTracking() {
enableLocationServices()
}
func stopTracking() {
locationManager.stopUpdatingLocation()
}
func resetPreviousLocation() {
previousLocation = nil
}
private override init() {}
}
extension LocationTracker: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
print(locations)
guard let location = locations.first else { return }
guard -location.timestamp.timeIntervalSinceNow < 120, // Validate only location fetched recently
location.horizontalAccuracy > 0, // Validate Horizontal Accuracy - Ve means Invalid
location.horizontalAccuracy < 200 // Validate Horizontal Accuracy > 100 M
else {
print("invalid location received OR ignore old (cached) updates")
return
}
self.locations.append(location)
lastLocation = location
if let activeRide = RideManager.shared.activeRide,
let _ = AccessTokenHelper.shared.accessToken,
let activeRideId = activeRide.ride_id,
let type = activeRide.rideStatusTypeOptional,
type == .started {
//Store Location For A particular Ride after Start
LocationUpdater.shared.saveInDataBase(rideId: activeRideId, locations: [location])
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error.localizedDescription)
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
enableLocationServices()
}
}
/*
This class having responsibility of Updating the location on server after n second and update path after n second.
**/
class LocationTimer {
static let time: Double = 30
}
/*
class to update locations to server after nth second
**/
class LocationUpdater: NSObject {
static let shared = LocationUpdater(n: Double(LocationTimer.time), tracker: LocationTracker.shared)
let n: Double
private let tracker: LocationTracker
var timer: Timer! = nil
init(n: Double, tracker: LocationTracker) {
self.n = n
self.tracker = tracker
super.init()
}
func startUpdater() {
self.timer?.invalidate()
self.timer = nil
self.timer = Timer.scheduledTimer(timeInterval: n, target: self, selector: #selector(updateLocationsToServer), userInfo: nil, repeats: true)
self.timer.fire()
}
func stopUpdater() {
self.timer?.invalidate()
self.timer = nil
}
#objc func updateLocationsToServer() {
// update to server
}
}
// usage
LocationTracker.shared.startTracking()
LocationUpdater.shared.startUpdater()

SWIFT: Core Location returning nil - how to call didUpdateLocations?

I have an issue with Core Location, I've followed the setup from
https://stackoverflow.com/a/24696878/6140339 this answer, and placed all my code in AppDelegate, but I can't figure out how to call it, so I created another function that does the same thing as didUpdateLocations, and called it inside my findMyLocation button, and it is only returning nil.
I have tried setting a custom location in Simulator, still nil, I tried using the debugger and setting a location, I even tried testing it on my iphone to see if i could get a location, and still nothing.
is there a way to call didUpdateLocations from my button?
Or am I just doing something else wrong that im missing.
here is my code in my viewController:
import UIKit
import Social
import CoreLocation
class FirstViewController: UIViewController{
//let social = socialFunctions()
let locationManager = CLLocationManager()
let location = locationFunctions()
var locationFixAchieved = false
var currentLocation = CLLocation()
#IBOutlet weak var locationLabel: UILabel!
#IBAction func findMyLocation(sender: AnyObject) {
updateLocation()
print("Location: \(locationManager.location)")
}
#IBAction func postToFacebookButton(sender: UIButton) {
postToFacebook()
}
#IBAction func postTweetButton(sender: UIButton) {
postToTwitter()
}
override func preferredStatusBarStyle() -> UIStatusBarStyle {
return .LightContent
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
//MARK: - SOCIAL FUNCTIONS
func postToFacebook(){
if(SLComposeViewController.isAvailableForServiceType(SLServiceTypeFacebook)){
let socialController = SLComposeViewController(forServiceType: SLServiceTypeFacebook)
//creates post with pre-desired text
socialController.setInitialText("")
presentViewController(socialController, animated: true, completion: nil)
}
}
func postToTwitter(){
if(SLComposeViewController.isAvailableForServiceType(SLServiceTypeTwitter)){
let socialController = SLComposeViewController(forServiceType: SLServiceTypeTwitter)
//creates post with pre-desired text
socialController.setInitialText("")
presentViewController(socialController, animated: true, completion: nil)
}
}
//MARK: - LOCATION FUNCTIONS
func updateLocation() {
let locations = [CLLocation]()
if (locationFixAchieved == false) {
locationFixAchieved = true
let locationArray = locations as NSArray
let locationObj = locationArray.lastObject as? CLLocation
let coord = locationObj?.coordinate
if coord?.latitude != nil {
locationLabel.text = ("Location \r\n Latitude: \(coord?.latitude) \r\n Longitude: \(coord?.longitude)")
print("Latitude: \(coord?.latitude)")
print("Longitude: \(coord?.longitude)")
} else {
locationLabel.text = ("Could not find location")
print("LAT & LONG are nil")
}
}
}
}
Here is the code i added to my appDelegate
import UIKit
import CoreLocation
let fvc = FirstViewController()
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, CLLocationManagerDelegate {
var window: UIWindow?
var locationManager: CLLocationManager!
var seenError : Bool = false
var locationFixAchieved: Bool = false
var locationStatus : NSString = "Not Started"
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject : AnyObject]?) -> Bool {
initLocationManager();
return true
}
func initLocationManager() {
seenError = false
locationFixAchieved = false
locationManager = CLLocationManager()
locationManager.delegate = self
CLLocationManager.locationServicesEnabled()
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestAlwaysAuthorization()
}
func locationManager(manager: CLLocationManager, didFailWithError error: NSError) {
locationManager.stopUpdatingLocation()
if (error == true) {
if (seenError == false) {
seenError = true
print(error)
}
}
}
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let locations = [CLLocation]()
if (locationFixAchieved == false) {
locationFixAchieved = true
let locationArray = locations as NSArray
let locationObj = locationArray.lastObject as? CLLocation
let coord = locationObj?.coordinate
print("Latitude: \(coord?.latitude)")
print("Longitude: \(coord?.longitude)")
//fvc.locationLabel.text = ("Location \r\n Latitude: \(coord?.latitude) \r\n Longitude: \(coord?.longitude)")
}
}
func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
var shouldIAllow = false
switch status {
case CLAuthorizationStatus.Restricted:
locationStatus = "Restricted Access to location"
case CLAuthorizationStatus.Denied:
locationStatus = "User denied access to location"
case CLAuthorizationStatus.NotDetermined:
locationStatus = "Status not determined"
default:
locationStatus = "Allowed location Access"
shouldIAllow = true
}
NSNotificationCenter.defaultCenter().postNotificationName("LabelHasBeenUpdated", object: nil)
if (shouldIAllow == true) {
NSLog("Location Allowed")
//Start location services
locationManager.startUpdatingLocation()
} else {
NSLog("Denied access: \(locationStatus)")
}
}
Following dan's comment(thank you) all I had to do was delete the first line, and it shows the coordinates, i have yet to figure out how to call my function to change the label text, but i will post that when i figure it out. EDIT: posted solution below
I changed
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let locations = [CLLocation]()
if (locationFixAchieved == false) {
locationFixAchieved = true
let locationArray = locations as NSArray
let locationObj = locationArray.lastObject as? CLLocation
let coord = locationObj?.coordinate
print("Latitude: \(coord?.latitude)")
print("Longitude: \(coord?.longitude)")
//fvc.locationLabel.text = ("Location \r\n Latitude: \(coord?.latitude) \r\n Longitude: \(coord?.longitude)")
}
}
deleting let locations = [CLLocation]()
This is how i called it when i press the button.
func updateLocation() {
let manager = CLLocationManager()
let locValue : CLLocationCoordinate2D = (manager.location?.coordinate)!;
let long = locValue.longitude
let lat = locValue.latitude
print(long)
print(lat)
locationLabel.text = ("Location \r\nLatitude: \(lat) \r\nLongitude: \(long)")
}

Resources