Once a button is pressed, I have a CLLocationManager that requests authorization for location services and if the user accepts, the locationManager will call startUpdatingLocation(). Once there is an updated location (which is immediately), I expect the ClLocationManagerDelegate to call didUpdateLocations and from there I immediately call manager.stopUpdatingLocation() so that I ONLY get 1 set of coordinates for the time being. However, sometimes (inconsistently) I will get two sets of coordinates as if the button was pressed twice in succession. I understand startUpdatingLocation can be tricky because it can very rapidly update your location until it is stopped but I can't seem to pinpoint where and how to avoid this! I found many threads on this same issue but nothing that has worked for my specific case.
I looked online and found this and tried a couple of the things in that thread but still could not fix it.
Below is my code:
getUserLocation() is the first function called when the button in my app is pressed.
func getUserLocation() {
self.places.removeAll()
self.placesTableView.reloadData()
LocationService.shared.requestPermissionToAccessLocation()
LocationService.shared.locationUpdated = { [weak self] location in
self?.fetchPlaces(location: location)
}
self.view.addSubview(placesTableView)
placesTableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate(
[
placesTableView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 200),
placesTableView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 150),
placesTableView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -10),
placesTableView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -130),
]
)
}
private func fetchPlaces(location: CLLocationCoordinate2D) {
let searchSpan = MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1)
let searchRegion = MKCoordinateRegion(center: location, span: searchSpan)
let searchRequest = MKLocalSearch.Request()
searchRequest.region = searchRegion
searchRequest.resultTypes = .pointOfInterest
searchRequest.naturalLanguageQuery = "bar"
let search = MKLocalSearch(request: searchRequest)
search.start { response, error in
guard let mapItems = response?.mapItems else {
return
}
DispatchQueue.main.async { [weak self] in
for item: MKMapItem in mapItems {
let placeMark = item.placemark as CLPlacemark
let completeBusinessString = String(format: "%#\n%# %#\n%#, %# %#", placeMark.name!, placeMark.subThoroughfare!, placeMark.thoroughfare!, placeMark.locality!, placeMark.administrativeArea!, placeMark.postalCode!)
self?.places.append(completeBusinessString)
}
self?.placesTableView.reloadData()
}
}
}
LocationServices singleton class
class LocationService: NSObject {
static let shared = LocationService()
lazy var locationManager: CLLocationManager = {
let manager = CLLocationManager()
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.delegate = self
return manager
}()
var locationUpdated: ((CLLocationCoordinate2D) -> Void)?
override private init() {
super.init()
}
func requestPermissionToAccessLocation() {
switch locationManager.authorizationStatus {
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
case .restricted:
locationManager.requestWhenInUseAuthorization()
case .denied:
break
case .authorizedAlways:
locationManager.startUpdatingLocation()
case .authorizedWhenInUse:
locationManager.startUpdatingLocation()
default:
break
}
}
}
extension LocationService: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
manager.stopUpdatingLocation()
if let location = locations.last?.coordinate {
print(location)
locationUpdated?(location)
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error.localizedDescription)
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
switch manager.authorizationStatus {
case .authorizedAlways:
manager.startUpdatingLocation()
case .authorizedWhenInUse:
manager.startUpdatingLocation()
default:
break
}
}
}
If the goal is to get just one value that tells you the location, you're making the wrong call; use requestLocation.
Related
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 🤦♂️
I am using location.requestLocation in swift 5 for my ios app. But its taking way too long time. around 10 seconds. searching for solutions online, they are saying use startUpdatingLocation instead..but problem is I only want it once... not continuous update. I am also not interested in it being accurate.I can have it wrong by some margin.
Because all I want is just zoom to my google map position, and sort stuff by distance using user location. Location can be wrong up to 3 - 4 Kilometers I have no problem.
override func viewDidLoad() {
super.viewDidLoad()
//mapView.showsUserLocation = false
locationManager.delegate = self
print(locationManager.distanceFilter)
switch CLLocationManager.authorizationStatus() {
case CLAuthorizationStatus.notDetermined, .restricted, .denied:
locationManager.requestWhenInUseAuthorization()
case CLAuthorizationStatus.authorizedWhenInUse, .authorizedAlways:
requestLocation()
#unknown default: break
}
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
print("\nStart of locationManager(didChangeAuthorization)")
let authStatus = CLLocationManager.authorizationStatus()
if authStatus == CLAuthorizationStatus.authorizedWhenInUse
|| authStatus == CLAuthorizationStatus.authorizedAlways {
requestLocation()
}
print("\nEnd of locationManager(didChangeAuthorization)")
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
print("\nStart of locationManager(didUpdateLocations)")
zoomInLocation(locations.last!)
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
if let err = error as? CLError, err.code == .denied {
manager.stopUpdatingLocation()
return
}
print("\nlocationManager(): \(error.localizedDescription)")
}
private func requestLocation() {
print("\requestLocation() called")
// check if the location service is availalbe on that device
if !CLLocationManager.locationServicesEnabled() {
return
}
locationManager.requestLocation()
}
private func zoomInLocation(_ location: CLLocation) {
loca2 = location
let camera = GMSCameraPosition.camera(withLatitude: location.coordinate.latitude, longitude: location.coordinate.longitude, zoom: 6.0)
(view1 as! GMSMapView).animate(to: camera)
print("\nzoomInUserLocation(): mapView[latitude]=\(location.coordinate.latitude), locationManager[latitude]=\(String(describing: location.coordinate.latitude))")
let coordinateSpan = MKCoordinateSpan(latitudeDelta: 20, longitudeDelta: 20)
_ = MKCoordinateRegion(center: location.coordinate, span: coordinateSpan)
// mapView.centerCoordinate = location.coordinate
//mapView.setRegion(coordinateRegion, animated: true)
if(i1 != 0){
self.sortByDistance()
}
}
So I have been able to get the users location via the following code.
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status != .authorizedWhenInUse {return}
print("test LOCATION BELOW")
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.startUpdatingLocation()
let locValue: CLLocationCoordinate2D = manager.location!.coordinate
print("UUID: \(String(describing: uuid)) locations = \(locValue.latitude) \(locValue.longitude)")
}
However I want to watch the users location and if they move update their location.
I am wondering how do I get this code to keep checking for the users location?
My
override func viewDidLoad(){
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.requestWhenInUseAuthorization()
locationManager.requestAlwaysAuthorization()}
I get it popping up the request and I approve it but it does not run the code
Here is how you can get the location data. The code is similar to yours but with a bit of change and additions.
First check if you already have user's permission to get their location data.
func isLocationServicesEnabled() -> Bool {
if CLLocationManager.locationServicesEnabled() {
switch(CLLocationManager.authorizationStatus()) {
case .notDetermined, .restricted, .denied:
return false
case .authorizedAlways, .authorizedWhenInUse:
return true
#unknown default:
return false
}
}
return false
}
If this method returns false you can ask for authorization.
// Initialize manager in your viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
locationManager = CLLocationManager()
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
locationManager.delegate = self
// Do other stuff
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Check for auth
if isLocationServicesEnabled() {
locationManager.startUpdatingLocation()
} else {
locationManager.requestWhenInUseAuthorization()
}
// Do other stuff
}
Finally in your CLLocationManagerDelegate implementation get the coordinates.
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last?.coordinate else { return }
// Use location.latitude and location.longitude here
// If you don't want to receive any more location data then call
locationManager.stopUpdatingLocation()
}
First, you have to check that you have user's permission to get their location data
func checkLocationServicesAuthorization() -> Bool {
if CLLocationManager.locationServicesEnabled() {
switch CLLocationManager.authorizationStatus() {
case .notDetermined:
return false
case .restricted, .denied:
//code for open settings screen in user’s phone
return false
case .authorizedWhenInUse, .authorizedAlways:
return true
default:
return false
}
} else {
//code for open settings screen in user’s phone
}
return false
}
And
override func viewDidLoad() {
super.viewDidLoad()
locationManager = CLLocationManager()
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
locationManager.delegate = self
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if checkLocationServicesAuthorization() {
locationManager.startUpdatingLocation()
} else {
locationManager.requestWhenInUseAuthorization()
}
}
You can use delegate method didUpdateLocations of CLLocationManagerDelegate and don't forgot to connect delegate yourLocationManager.delegate = self
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let userLocation:CLLocation = locations[0] as CLLocation
// Call stopUpdatingLocation() to stop listening for location updates, other wise this function will be called every time when user location changes.
// yourLocationManager.stopUpdatingLocation()
guard let locValue: CLLocationCoordinate2D = manager.location?.coordinate else { return }
let latitude = String.init(userLocation.coordinate.latitude)
let longitude = String.init(userLocation.coordinate.longitude)
print("\(latitude), \(longitude)")
}
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()
I'm trying to get location updates for workout tracking (indoors) and so I am in need of very precise and constant location updates, but in testing the delegate callbacks don't seem to be very accurate. For example, moving 20-30 feet doesn't trigger a location update most of the time. Is there anything in my code below that might cause this inaccuracy?
import CoreLocation
protocol UserLocationDelegate: class {
func didUpdateUserLocation(_ manager: WorkoutLocationManager, distance: CLLocationDistance)
}
class WorkoutLocationManager: NSObject, CLLocationManagerDelegate {
deinit {
self.locationManager?.stopUpdatingLocation()
}
private var locationManager: CLLocationManager?
var previousLocation: CLLocation?
weak var userLocationDelgate: UserLocationDelegate?
public func getUserLocation() {
guard CLLocationManager.locationServicesEnabled() else {
print("User does not have location services enabled")
return
}
locationManager = CLLocationManager()
locationManager?.delegate = self
locationManager?.allowsBackgroundLocationUpdates = true
locationManager?.desiredAccuracy = kCLLocationAccuracyBest
locationManager?.activityType = .fitness //test as the docs say this will turn OFF indoor tracking
let locationAuthorizationStatus = CLLocationManager.authorizationStatus()
switch locationAuthorizationStatus {
case .authorizedAlways:
print("location authorized Always")
locationManager?.startUpdatingLocation()
case .authorizedWhenInUse:
print("location authorized When in Use")
locationManager?.startUpdatingLocation()
case .denied:
print("location authorization denied")
locationManager?.requestAlwaysAuthorization()
case .notDetermined:
print("location authorization not determined")
locationManager?.requestAlwaysAuthorization()
case .restricted:
print("location authorization restricted")
locationManager?.requestAlwaysAuthorization()
}
}
// MARK: - CLLocationManagerDelegate
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
print("did update locations called")
if previousLocation == nil {
previousLocation = locations.first
} else {
guard let latest = locations.first else { return }
let distanceInMeters = previousLocation?.distance(from: latest) ?? 0
if distanceInMeters > 0 {
let distanceInFeet = distanceInMeters * 3.28
print("distance in feet: \(distanceInFeet)")
userLocationDelgate?.didUpdateUserLocation(self, distance: distanceInFeet
)
}
previousLocation = latest
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("location manager error = \(error)")
}
}
import WatchKit
import Foundation
import CoreLocation
class InterfaceController: WKInterfaceController, UserLocationDelegate {
func didUpdateUserLocation(_ manager: WorkoutLocationManager, distance: CLLocationDistance) {
locationLabel.setText("\(distance.rounded().description) feet")
}
let workoutLocationManager = WorkoutLocationManager()
#IBOutlet weak var locationLabel: WKInterfaceLabel!
override func awake(withContext context: Any?) {
super.awake(withContext: context)
workoutLocationManager.getUserLocation()
workoutLocationManager.userLocationDelgate = self
}
You set kCLLocationAccuracyBest as desiredAccuracy. There is variable for CLLocationAccuracy which should be more accurate
kCLLocationAccuracyBestForNavigation