Why is the Location Manager calling startUpdatingLocation more than once? Sometimes it is calling once, other times it is calling it three times. I don't know why; maybe you could help me. I have this code from GitHub.
import UIKit
import CoreLocation
class ViewController: UIViewController, CLLocationManagerDelegate
{
let locationManager = CLLocationManager()
override func viewDidLoad()
{
super.viewDidLoad()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
}
override func didReceiveMemoryWarning()
{
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
CLGeocoder().reverseGeocodeLocation(manager.location!, completionHandler: {(placemarks, error) -> Void in
if (error != nil)
{
print("Error: " + error!.localizedDescription)
return
}
if placemarks!.count > 0 {
if let pm = placemarks?.first {
self.displayLocationInfo(pm)
}
}
else
{
print("Error with the data.")
}
})
}
func displayLocationInfo(placemark: CLPlacemark)
{
self.locationManager.stopUpdatingLocation()
print(placemark.locality)
print(placemark.postalCode)
print(placemark.administrativeArea)
print(placemark.country)
}
func locationManager(manager: CLLocationManager, didFailWithError error: NSError)
{
print("Error: " + error.localizedDescription)
}
}
Yes, this is standard behavior. When you start location services you will generally receive a series of increasingly accurate CLLocation updates (i.e. with horizontalAccuracy decreasing over time) as the device "warms up". For example, it might start reporting location information that it might already have on the basis of cell towers, but as the GPS chip gets more information by which it can better triangulate your location, it will give you updates. Etc.
If you want to reduce this behavior, you can use a combination of a larger distanceFilter, a lower desiredAccuracy, or call stopUpdatingLocation once you get a location that you will geocode.
Right now you are calling stopUpdatingLocation, but you're doing it from the asynchronously called closure of reverseGeocodeLocation. This means that more location updates are able to slip in before the completion handler of reverseGeocodeLocation is called. If you call stopUpdatingLocation synchronously (e.g. before reverseGeocodeLocation), then you will avoid this behavior.
Related
I'm working on an App Shortcut using the new AppIntents framework in iOS 16 and I'm trying to get the user's current location, everything is enabled and set-up correctly with the permissions
func perform() async throws -> some IntentResult {
//Request User Location
IntentHelper.sharedInstance.getUserLocation()
guard let userCoords = IntentHelper.sharedInstance.currentUserCoords else { throw IntentErrors.locationProblem }
//How to wait for location??
return .result(dialog: "Worked! Current coords are \(userCoords)") {
IntentSuccesView()
}
}
And here is the IntentHelper class
class IntentHelper: NSObject {
static let sharedInstance = IntentHelper()
var currentUserCoords: CLLocationCoordinate2D?
private override init() {}
func getUserLocation() {
DispatchQueue.main.async {
let locationManager = CLLocationManager()
locationManager.delegate = self
print("FINALLY THIS IS IT")
self.currentUserCoords = locationManager.location?.coordinate
print(self.currentUserCoords)
}
}
}
extension IntentHelper: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error)
manager.stopUpdatingLocation()
}
}
Problem is, this sometimes, very rarely works, most of the times it prints nil, so how would you go about waiting for the location?
The problem is you are trying to get the location synchronously, so it only works if locationManager.location was already not nil by the time you ask for it. Instead this operation may take time and is therefore asynchronous.
So the basic flow is like this:
Check permissions (yes, you have to do it every time, as user may take away the permissions a any point)
And tell CLLocationManager to start resolving user location
After that just listen for result via locationManager(:, didUpdateLocations:) event of the CLLocationManagerDelegate, which
you need to implement (in your case in the same class, as you already
implemented the failure case in extension).
On top of that, you probably want to wait for location update (either coordinates or failure) inside your func perform().
So I would say you need to have something like this in func perform():
// Wait for coordinates
guard let userCoords = await IntentHelper.sharedInstance.getCurrentCoordinates() else { ... }
where the getCurrentCoordinates() is just an async wrapper, something like:
func getCurrentCoordinates() async -> CLLocationCoordinate2D? {
await withCheckedContinuation { continuation in
getCurrentCoordinates() { coordinates in
continuation.resume(returning: coordinates)
}
}
}
while getCurrentCoordinates(callback:) will be something like:
class IntentHelper {
var callback: ((CLLocationCoordinate2D?) -> Void)?
//...
func getCurrentCoordinates(callback: #escaping (CLLocationCoordinate2D?) -> Void) {
// Step 1: check permissions
let status = CLLocationManager.authorizationStatus()
guard status == .authorizedAlways || status == .authorizedWhenInUse else {
// you can't ask for permissions
callback(nil)
return
// Step 2: preserve callback and request location
self.callback = callback
locationManager?.requestLocation()
}
}
Now all you need to do is wait for locationManager(:, didUpdateLocations:) or locationManager(:, didFailWithError:) to happen:
extension IntentHelper: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
// Pass the result (no location info) back to the caller
self.callback?(nil)
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// Pass the result location back to the caller
// For simplicity lets say we take the first location in list
self.callback?(locations.first)
}
}
Note: this is a draft code, I didn't try to compile it, so you may need to fix some compilation errors.
Here's a nice walkthrough of the whole scenario (which also shows a nicer code organization (i.e. how to ask for permissions, etc).
I have a simple CLLocationManager implementation that works in one project but not in my new project.
The code is almost identical but I cannot get the .didUpdateLocations function to call. My code is below. Any ideas why I cannot get the update to work? I'm at a loss, I've build many apps using location services and never seen this situation.
Also I have the three settings in the PLIST set correctly for Privacy-Location Always etc.
There are no errors given, it simply doesn't call .didUpdateLocations
Weather Class
class DarkSkyWeatherController: UIViewController, CLLocationManagerDelegate {
var weatherGetterDelegate: DarkSkyWeatherControllerDelegate?
var locationManager = CLLocationManager()
var lat = String()
var long = String()
func getLocation() {
// Ask for Authorisation from the User.
locationManager.requestAlwaysAuthorization()
// For use in foreground
locationManager.requestWhenInUseAuthorization()
if CLLocationManager.locationServicesEnabled() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers
locationManager.startUpdatingLocation()
}
locationManager.delegate = self
locationManager.requestLocation()
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error)
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let locValue: CLLocationCoordinate2D = manager.location?.coordinate else {return}
print("locations = \(locValue.latitude) \(locValue.longitude)")
lat = String(locValue.latitude)
long = String(locValue.longitude)
getDarkSkyWeather { (fetchedInfo) in
if let myFetchedInfo = fetchedInfo {
self.weatherGetterDelegate?.getMyWeather(weather: myFetchedInfo)
}
}
}
ViewDidLoad in main window
let weather = DarkSkyWeatherController()
weather.weatherGetterDelegate = self
weather.getLocation()
Thanks for looking at this.
Without seeing your full main window code, I bet that the problem is with the scope and lifecycle of your controller:
override func viewDidLoad() {
let weather = DarkSkyWeatherController()
weather.weatherGetterDelegate = self
weather.getLocation()
// Function exits. The weather constant dies off.
// This is why you don't get callbacks.
}
Do the following, instead.
let weather = DarkSkyWeatherController()
override func viewDidLoad() {
weather.weatherGetterDelegate = self
weather.getLocation()
// Function exits, but the weather constant lives on as a field of your main ViewController. You'll get your callbacks now.
}
Can someone please help me understand why in the following code will have output like this:
[<+37.49638550,-122.31160280> +/- 5.00m (speed 32.65 mps / course
294.26) # 7/27/16, 4:34:19 AM Eastern Daylight Time]
n/a Belmont Belmont
Belmont 1
That is, why the variable locality will have value "n/a" when printed at a specific point whereas localityVC & localityGlobal never do? Is this purely a scope issue or something to do with CLGeocoder()? Thanks
import UIKit
import MapKit
import CoreLocation
var localityGlobal: String = "n/a"
class ViewController: UIViewController, CLLocationManagerDelegate {
let locationManager = CLLocationManager()
var localityVC: String = "n/a"
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
print("hello")
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
}
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
print(locations)
let userLocation: CLLocation = locations[0]
var locality: String = "n/a"
CLGeocoder().reverseGeocodeLocation(userLocation, completionHandler: {(placemarks, error) -> Void in
if error != nil {
print("CLGeocoder.reverseGeocodeLocation() failed")
return
}
if placemarks!.count > 0 {
locality = placemarks![0].locality!
self.localityVC = placemarks![0].locality!
localityGlobal = placemarks![0].locality!
print(locality, placemarks!.count)
} else {
print("CLGeocoder.reverseGeocodeLocation() error in geocoder data")
}
})
print(locality, localityVC, localityGlobal)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
In order for the above to run you also need to have:
Build Phases -> Link Binary with Libraries include
CoreLocation.framework,
Info.plist must have variables
NSLocationWhenInUseUsageDescription and
NSLocationAlwaysUsageDescription set,
Simulator -> Debug -> Location set to something like Apple, City Bicycle Ride, City Run or Freeway Drive
I'm running xcode 7.3.1 (7D1014)
This isn't a scope issue, it's a timing one. You get n/a because the closure has yet to run when that print statement is executed.
The other variables are retaining the value from a prior call.
For some reason I cannot seem to get the users location. I bought the iOS 9 CookBook and coped their code exactly
Here is the ViewController:
import UIKit
import CoreLocation
class ViewController: UIViewController, CLLocationManagerDelegate {
lazy var locationManager: CLLocationManager = {
let m = CLLocationManager()
m.delegate = self
m.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
return m
}()
func locationManager(manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]) {
//TODO: now you have access to the location. do your work
}
func locationManager(manager: CLLocationManager,
didFailWithError error: NSError) {
//TODO: handle the error
}
func locationManager(manager: CLLocationManager,
didChangeAuthorizationStatus status: CLAuthorizationStatus) {
if case .AuthorizedWhenInUse = status{
manager.requestLocation()
} else {
//TODO: we didn't get access, handle this
print("No Access Granted")
}
}
#IBAction func requestLocation() {
locationManager.requestWhenInUseAuthorization()
}
}
The output in the console is:
No Access Granted
Any ideas what is going on? I have tested this on both the simulator and the device
Edit: I have added a button to the storyboard and licked that to the IBAction accordingly.
Have you added the NSLocationWhenInUseUsageDescription key to your plist? - The value should be a string describing why you want to use the users location.
<key>NSLocationWhenInUseUsageDescription</key>
<string>Required to provide information about your location.</string>
If its all working correctly the didUpdateLocations delegate method should return an array of locations, you can print these out like so...
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
for loc in locations {
print(loc)
}
}
If you want to continually monitor the location you can call manager.startUpdatingLocation() instead of manager.requestLocation()
Here's a problem if I don't request "AuthorizedWhenInUse" status once my app first view controller is loaded I'll never get update after.
Let's say I have a map view controller. When I ask for status in the viewDidLoad method it updates my location, i.e. func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) is called.
Now I added an intro view controller, once user finishes intro he/she is going to the old map controller. But now func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) isn't called anymore!
What I noticed is if I go to settings and toggle authorization status manually for my application and then get back to my app the didUpdateLocations is called.
class MapViewController: UIViewController, CLLocationManagerDelegate {
private let locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
locationManager.distanceFilter = 100
locationManager.delegate = self
// >=iOS8
if (locationManager.respondsToSelector(Selector("requestWhenInUseAuthorization"))) {
locationManager.requestWhenInUseAuthorization()
} else {
locationManager.startUpdatingLocation()
}
}
// MARK: - CLLocationManagerDelegate
func locationManager(manager: CLLocationManager!, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
if status == .AuthorizedAlways || status == .AuthorizedWhenInUse {
manager.startUpdatingLocation()
}
}
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
if let location = locations.first as? CLLocation {
println("User's location: \(location.description)")
} else {
println("User's location is unknown")
}
}
}
I see several issues with what you posted, here are just two:
You are calling locationManager.startUpdatingLocation() from viewDidLoad this will only fire if the view is not already loaded into memory. Recommend moving this into viewWillAppear so it fires every time.
This code should be rewritten:
if (locationManager.respondsToSelector(Selector("requestWhenInUseAuthorization"))) {
locationManager.requestWhenInUseAuthorization()
} else {
locationManager.startUpdatingLocation()
}
You need to check the authorizationStatus instead, if it is kCLAuthorizationStatusNotDetermined then request permission using the if statement you have above. If you don't iOS 8 users will always drop into the requestWhenInUseAuthorization section. You don't want that because the OS will only ask for permission once. It will not ask again unless you rest your phones Location Privacy under Settings.