I have a problem in Objective C (writing an app for the iPhone). I know how to use the locationManager and its delegates. Once I called startUpdatingLocation on the locationManager, it will call the delegate method didUpdateToLocation whenever the location is updated. But the problem is, that this way is now suitable for what I want to do. I have a method (in a class) which looks as follows:
#implementation SomeClass
- (someType)getDbContentsOrderByDistance
{
... //here I need the current location
return dbContents;
}
#end
Now this function selects entries from a sqlite database ordered by their distance to the users current location. That is why I need the location in this function. What I want to do in the above method is: get the location, then select the stuff from the database and return it. I know that I could just try to access locationManager.location.coordinate.longitude, but I know that there won't be anything useful inside until the first location update has arrived. And I don't want to start the location updates before (e.g. when starting the app) because that would not be very efficient if I only need the location once.
I know how to do it the way that the delegate is called as soon as a location update arrives. The problem is that I don't know how to get that updated location from there into my method above. Basically I would need to let the method 'wait' until there is the first location update and then continue execution with that location.
Thank you for your help!
The short answer is, you can't. Getting the users location is an asynchronous process, and MUST be an asynchronous process. The device has to fire up various hardware like the GPS, cell tower locator, and WiFi triangulation system, get input from those different devices, and synthesize that into a location. That takes multiple seconds to do.
Putz's suggestion of starting a timer is a good one. I would add a few things however.
Typically the first readings you get from the location manager are really bad and should be discarded. The first reading you get is usually the last location reading when the GPS was active, and will have an out-of-date timestamp. The accuracy reading in that location might appear quite good, but it's a lie. I have seen the first location reading be off by several kilometers. You need to check the timestamp on your location updates and discard any reading that is more than 1 second old.
Once you've discarded the stale readings, the first several location updates are often really bad because the GPS hasn't settled down yet. The horizontal accuracy reading (which is really a "circle of confusion", or radius of possible positions) is an absurdly large value, sometimes a kilometer or more. Again, you need to write your location manager delegate method to discard readings that are too inaccurate. I suggest discarding values with a horizontal accuracy reading of >= 100 meters. How much inaccuracy you can tolerate depends on the specific appellation, but beware of making the accuracy requirement too accurate. Sometimes the GPS refuses to settle down. If you require a 5 meter accuracy, you might not get an acceptable accuracy during the entire run of your app. (If you're in an urban environment, a building, or other area where there is a lot of interference/obstruction of GPS signals)
Once you finally do get a value that's accurate enough, save the location and set your locationAvailable flag to YES.
Note that instead of a timer you could use the notification manager to broadcast a "got a good location" message. Any object that needs location information could register for your notification, and get called when the location manager gets an acceptable location reading.
Start a timer that fires every second or so that calls the function you need the location in. check to see if the location has been set. If the location is valid then kill the timer, if not then let the timer continue and check again in one second or so.
The way of knowing if the location is valid is in the delegate method. Set a global variable like locationAvailable = false. Then when the delegate method gets called set that variable to true.
Using location property can solve the purpose
From apple documentation
location The most recently retrieved user location. (read-only)
#property(readonly, nonatomic, copy) CLLocation *location
The value of this property is nil if no location data has ever been retrieved.
In iOS 4.0 and later, this property may contain a more recent location object at launch time. Specifically, if significant location updates are running and your app is terminated, this property is updated with the most recent location data when your app is relaunched (and you create a new location manager object). This location data may be more recent than the last location event processed by your app.
It is always a good idea to check the timestamp of the location stored in this property. If the receiver is currently gathering location data, but the minimum distance filter is large, the returned location might be relatively old. If it is, you can stop the receiver and start it again to force an update.
Whenever I create a new instance of location manager I get the most recent value of location in location property and since my distance filter is not set , it has the current timestamp. This means I dont have to call startUpdatingLocation and my data is accurate. I also need this for calculating distance between user location and a place so I can immediately return true or false if it is within the range. I find this very useful as I get location within 0.006 seconds and handy and got it from apple documentation only but still I dont know if it is a best practice.
Related
Background
According to the documentation for the delegate method locationManager(manager:locations:)
Parameter: locations [CLLocation]
An array of CLLocation objects containing the location data. This array always contains at least one object representing the current location. If updates were deferred or if multiple locations arrived before they could be delivered, the array may contain additional entries. The objects in the array are organized in the order in which they occurred. Therefore, the most recent location update is at the end of the array.
My question specifically is whether anyone knows exactly what they mean by the following sentence
If updates were deferred or if multiple locations arrived before they could be delivered, the array may contain additional entries.
Question
Could someone shed some light on what scenarios would lead to "deferred location updates" or "multiple location updates"?
Does this occur when someone has approximate location but not precise location enabled?
Does this occur when the user is switching between our app and another app very quickly?
Does this occur when GPS signal is not strong enough (e.g. inside an elevator)?
Does this occur when someone has approximate location but not
precise location enabled?
No
Does this occur when the user is switching between our app and another app very quickly?
No
Does this occur when GPS signal is not strong enough (e.g. inside an
elevator)?
Can be, Location Service (or CLLocationManager) will try to get the best location that matches your configuration for CLLocationManager. So it can return a list of coordinates after a delay to wait for the best location.
For the deferred update, you can check this:
https://developer.apple.com/documentation/corelocation/cllocationmanager/1620547-allowdeferredlocationupdates
If you look at a map, the radius of accuracy begins big and then gets smaller as it gets more accurate. But locationManager:didupdateLocations: doesn't get called again when that changes.
Is there something that is called on that event?
I believe that the didUpdateLocations: callback will actually be called again when accuracy is updated. According to this Apple docs on Core Location:
In addition to a location object’s timestamp, you can also use the
accuracy reported by that object to determine whether you want to
accept an event. As it receives more accurate data, the location
service may return additional events, with the accuracy values
reflecting the improvements accordingly. Throwing away less accurate
events means your app wastes less time on events that can’t be used
effectively anyway.
What sort of tests have you tried to improve accuracy? Did you try it on an iPhone? Did you set the delegate and the desiredAccuracy property properly?
If you provide the following code, you will get very frequent location updates.
self.locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation;
But according to Apple, it is recommended to plug in the charger to the iOS device while location manager is active. This is because the battery power gets drained fast while getting this much location updates.
There are other levels of accuracy for CLLocationManager( kCLLocationAccuracyBestForNavigation, kCLLocationAccuracyBest, kCLLocationAccuracyKilometer, kCLLocationAccuracyHundredMeters etc..)
Is there something that is called on that event?
No. You continue to get the same callbacks, but the accuracy of each location is (potentially) improved. There is no callback for "accuracy improved".
The CLLocations you receive in the callback contains vertical and horizontal accuracy information.
I have added [CLLocation allowDeferredLocationUpdatesUntilTraveled: timeout] to my app. Most of the time everything works fine. Every now and then I get the current location when I turn on the screen in the middle of the deferred locations. Is it possible that the location manager would send my app a locationManager:didUpdateLocations: for the current location and then call it with an array of deferred locations? About the locations passed to locationManager:didUpdateLocations Apple's docs say
This array always contains at least one object representing the
current location. If updates were deferred or if multiple locations
arrived before they could be delivered, the array may contain
additional entries. The objects in the array are organized in the
order in which they occurred. Therefore, the most recent location
update is at the end of the array.
I didn't see anything about the order in which didUpdateLocations is called. Is there any guarantee that the delegate method didUpdateLocations is called in the correct order?
Thanks!
Stephen
So I found the answer to this on developer.apple.com (https://devforums.apple.com/thread/251363?tstart=0). Basically, yes this can happen. When the device wakes up it first sends some individual locations for your current location and then sends the deferred locations. This matches the behavior I see. Here is the response from developer.apple.com.
I was "sanitising" the time stamps so that my subsequent code would
not be confused by time going backwards. I thought I had allowed for
this in deferred mode by sorting the batch of deferred locations by
time stamp on receipt, but actually I wasn't doing enough. What
actually seems to happen when the device wakes after a deferred period
is that it sends a handful of individual locations first, with the
current time stamp, and then sends the large batch of saved GPS fixes.
So the lesson is that if you're going to try to sort your location
fixes you need to process more than just a single deferred batch. You
really need to merge the new locations into your previously-received
locations, going back as far as necessary. it's unfortunate that
deferred mode is so difficult to debug.
This is stated in CLLocationManager class reference:
When requesting high-accuracy location data, the initial event delivered by the location service may not have the accuracy you requested.
This is really affecting my app. How can I make sure that the location found is the one with the correct accuracy?
I tried to use the 4th or 5th update rather than first retrieved location but this is not a good solution. Any idea?
You should check the accuracy of the updates, CLLocation
contains a property horizontalAccuracy which you can use to check the accuracy.
When the CLLocation has an accuracy that you find accurate enough you use and ignore al others.
Als you should tel CLLocationManager your desired accuracy. To do this set the desiredAccuracy property in CLLocationManager.
I think you will have to live with that. That's how Apple implemented it. Getting a fine grained position takes time, just think about how long any windshield-mounted GPS devices in cars take to fix up their position.
So instead of letting your application wait for a longer time, they provide with what accuracy is available almost immediately, based on cell-towers and WiFi hotspots in the vicinity. Only when there has been a more reliable GPS fix will they call into your app again and let you know.
In the end, it is just a question of where the waiting for the fine-grained position is: In your app, where you have the chance of doing something with the more coarse-grained data you get quickly, or in their framework with no chance for apps to do anything useful in the meantime. I think, letting you decide is the better choice.
Depending on the type of app, you could have a map that automatically zooms in deeper as soon as better position data comes in, draw a smaller circle around the position you are expecting etc. For the end user, nothing's worse than waiting without getting any feedback. So even though this is probably not the answer you would have liked, I advise to make the best of it from a user's perspective.
I've been using CLLocationManager alot these days with mixed results. I'm able to get locations from the GPS and also significant location changes when the app is running in the background.
However, I always seem to get a course of -1.0, which (according to the documentation) indicates an invalid course.
In my didUpdateToLocation method I'm simply doing the following:
double courseDegrees = newLocation.course;
Am I missing something?
EDIT: Yes, I've tried it while moving.
From the docu of CLLocationManager Class Reference:
startMonitoringSignificantLocationChanges
This interface delivers new events only when it detects changes to the
device’s associated cell towers, resulting in less frequent updates
and significantly lower power usage.
To receive a course you need GPS, and not cell tower locating. If you need course then you
have to change to use startUpdatingLocation start using
desiredAccuracy= AccuracyBestForNavigation