After analysing location services in iOS 10, found that some inconsistency is in the caching behaviour.
Fetching locations in a periodic time (in my case every 20 secs) returns locations but their timestamps are not in chronologically ordered. This indicates that the caching locations might have issues. So if you are checking accuracy through location-timestamp better to save the previous timestamps also. So that you could decide that the location fetched can be used or not.
Below image is taken from my console log. Here I used the format "Lat Long : latitude_longitude | location_timestamp | Now : current_timestamp"
Yes some time in best accuracy ios take the location from the cache so you need to avoid that location here is the code for accurate locationtion.
Update :
"Because it can take several seconds to return an initial location, the location manager typically delivers the previously cached location data immediately and then delivers more up-to-date location data as it becomes available. Therefore it is always a good idea to check the timestamp of any location object before taking any actions."
Reference :
https://developer.apple.com/reference/corelocation/cllocationmanager
Note: you can vary the accuracy for the device like ipod and ipad
//MARK: Location delgates
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
{
if locations.count > 0
{
let location = locations[locations.count-1]
let maxAge:TimeInterval = 60;
let requiredAccuracy:CLLocationAccuracy = 100;
let locationIsValid:Bool = Date().timeIntervalSince(location.timestamp) < maxAge && location.horizontalAccuracy <= requiredAccuracy;
if locationIsValid
{
NSLog(",,, location : %#",location);
NSLog("valid locations.....");
}
}
}
The problem behind this is that sometimes the timestamps do not match to the location! E.g. while traveling you suddenly records speeds > 300km/h and best accuracy.
I would sort the locations and only would take the last one if not too old:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let sortedLocations = locations.sorted { (l1, l2) -> Bool in
return l1.timestamp.compare(l2.timestamp) != .orderedDescending
}
if let newestLocation = sortedLocations.last{
if Date().timeIntervalSince(newestLocation.timestamp) < 60{
//TODO: Use the location
}
}
}
Yes like #chirag shah commented we definitely need to do that check. My suggestion is we should know about that the caching technique has been modified. And also it is not enough only checking the timestamp, we have to focus on the failure case. Here is the objective C code
-(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations{
CLLocation* location = [locations lastObject];
NSDate* locationTimestamp = location.timestamp;
NSTimeInterval cachedBefore = [locationTimestamp timeIntervalSinceNow];
if (fabs(cachedBefore) < 60.0) {
// Implement your code here
}else{
// Try again or wait for fetching location
}
}
Related
I'm developing an iOS application that users can save their traveling route to server (by posting their locations through API). The issue I am struggling with is several users reported that the route they saved is interrupted in the middle of the trip. In detail, when users review their saved route on map, there is a part of route being just a straight line because somehow locations of that route section are not sent to server or just because the device can not receive locations at that time.
The weird thing is that the rest of route was recorded normally so seems like the location service stopped working for a period of time but after that it started again so my app could record it fine.
And the most frustrating thing is that I can not reproduce this issue.
Here are circumstances of the issue that user reported:
- User started the app then locked device screen and put it in their pocket, they did not touch it in the whole journey. No battery drain or crash happened.
- After driving about 8-9km and everything worked fine, route recording was interrupted in the next ~ 65km, then well-recorded again in the rest ~ 80km.
Below is my project setup:
- Background Modes in ON in Capabilities with Location updates.
- Locations received from location service are filtered based on timestamp and accuracy and saved to core data with a “isSent” flag marking if a location is sent successfully to server. This way my app can cover the case when network connection is down.
- Locations marked with false “isSent” flag will be sent to server every 30 seconds.
My LocationManager code:
class LocationManager: NSObject, CLLocationManagerDelegate {
var locationManager: CLLocationManager = {
var _locationManager = CLLocationManager()
_locationManager.delegate = self
_locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
_locationManager.activityType = .automotiveNavigation
_locationManager.allowsBackgroundLocationUpdates = true
_locationManager.pausesLocationUpdatesAutomatically = false
return _locationManager
}()
func startLocationService() {
locationManager.startUpdatingLocation()
locationManager.allowsBackgroundLocationUpdates = true
locationManager.pausesLocationUpdatesAutomatically = false
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
var positionsToSavedToDb: [DbPositionModel] = []
for location in locations {
let howRecent = location.timestamp.timeIntervalSinceNow
guard abs(location.timestamp.timeIntervalSinceNow) < 10 && location.horizontalAccuracy > 0 && location.horizontalAccuracy < 33 else {
continue
}
if self.locations.count > 0 {
let distanceSinceLastLocation = location.distance(from: self.locations.last!)
let timeSinceLastLocation = location.timestamp.timeIntervalSince(self.locations.last!.timestamp)
if (distanceSinceLastLocation < 5 && abs(timeSinceLastLocation) < 10) {
continue
}
}
// Create DbPositionModel from location and append to positionsToSavedToDb
}
// Save positionsToSavedToDb to core data
}
#objc func each30Seconds(_ timer: Timer) {
// Select in database DbPositionModel objects with false “isSent” flag to send to server
}
}
Can you guys help me find out black holes in my code or anything I can do to reproduce / fix this issue? Thanks a lot!!!
Your setup looks fine to me. Just one question. When you say it didn't work for 60km and then it started working for 80km, was that all while in background? I mean the user didn't need to enter foreground for it to start working again did they?
Your limit for location.horizontalAccuracy is 33. You're thinking that it's going to be very accurate. I'm not sure, maybe the device/city are a bad combination, and then you're returning early. I suggest that you log the reason why you exit early. Their city might be different from yours. Also I've heard that the GPS of the iPhoneX even though has the correct location, it returns a high number for its horizontalAccuracy. Is this happening mostly for iPhoneX users?
enum LocationAccuracyError: Error {
case stale(secondsOld: Double)
case invalid
case lowAccuracy(metersOff: Double)
}
extension LocationAccuracyError: LocalizedError{
var errorDescription: String? {
switch self {
case .stale(let seconds):
return NSLocalizedString("location was stale by: \(seconds) seconds", comment: "")
case .invalid:
return NSLocalizedString("location was invalid)", comment: "")
case .lowAccuracy(let metersOff):
return NSLocalizedString("location's horizontal Accuracy was off by likely more than: \(metersOff) meters" , comment: "")
}
}
}
And then have a function like this to check each location.
private func checkLocationAccuracy(from location: CLLocation) throws {
let ageOfLocation = -location.timestamp.timeIntervalSinceNow
if ageOfLocation >= maximumAcceptedStale {
throw LocationAccuracyError.stale(secondsOld: ageOfLocation)
}
if location.horizontalAccuracy <= 0 {
throw LocationAccuracyError.invalid
}
if location.horizontalAccuracy > MaximumAcceptedHorizontalAccuracy{
throw LocationAccuracyError.lowAccuracy(metersOff: location.horizontalAccuracy)
}
}
Your end usage would be like:
do {
try checkLocationAccuracy(from: location)
} catch let error {
writelog("Bad Location: \(error.localizedDescription)")
}
I'd also add logs around your app state as well e.g. add a log to capture didEnterBackground
I have two constraints in my app, one is user location and the other one is time. Following is the simple location implementation.
func determineMyCurrentLocation() {
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.allowsBackgroundLocationUpdates = true
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
locationManager.requestAlwaysAuthorization()
locationManager.distanceFilter = 20
if CLLocationManager.locationServicesEnabled() {
locationManager.startUpdatingLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
print("*******Location updated*******")
}
With this code, I expect to get triggered every time the user location changes by 20m (in background as well). However, I also need to track the time the user stays in a particular location. Since i need to track the background case, I cannot use timer.
I followed https://www.raywenderlich.com/143128/background-modes-tutorial-getting-started to try background task but as stated in the article, the background time allowed that I got was around 3 min (which is variable). So i believe I cannot go with this.
How can I solve this?
EDIT: I also need to make an api call if the user stays in a location for X minutes. So for that case, it would not be feasible to wait for location updates and calculate the time differences. I could previously solve this by removing the distance filter and continuously checking the location and comparing the time and location. I guess continuous location tracking will get the app rejected, that's why I went for filter. But i am not sure if it will still get rejected or not since iOS will need to track the location for filter as well.
You can use simple Date objects to track the time spent between location updates even if your app is in the background. Simply declare a Date property for your class, update its value from func locationManager(_:, didUpdateLocations:) and compare that to the current time.
// Declare this in a scope that can be accessed from `didUpdateLocations` and where its value won't be released from memory
var lastUpdateTimestamp = Date()
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let now = Date()
let timeSinceLastUpdate = now.timeIntervalSince(lastUpdateTimestamp)
print("\(timeSinceLastUpdate) seconds passed since last location update")
lastUpdateTimestamp = now
}
Response to the question edit: making an API call after X minutes have passed in the background without a location update is not possible, since there's no supported background mode for executing arbitrary functions at specific points in time. Getting rid of the distance filter as you explained in your edit could be a valid alternative, however if your app is only using location updates to be able to make the API calls and isn't actually doing anything with those locations, your app might indeed get rejected.
When you receive a new location update, check the time difference from the previous update.. giving you the time that they were at the last location.
Depending on what you are doing with these location updates, this might just be pulling the latest update from some DB, or posting to an API.
An example..
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
print("*******Location updated*******")
let lastUpdateDate = // get the last datetime you received information
let currentDate = Date()
let minutesDifference = let componentsLeftTime = Calendar.current.dateComponents([.minute], from: lastUpdateDate, to: currentDate)
}
I’m coding a project in where I need to know the user’s exact position. (LocationManager)
The problem is that for example underground (metro) due to the weak signal the LocationManager can not determine the user’s location, there for it gives back cached location only..
What I do now is that I check whether the location is too old, if so then wait for a new one..
Okay but there’s a problem.. if user doesn’t move at all then location won’t get updated aswell because it will be only a cached location.. the timestamp check will say its too old..
How can I fix that?
You can check cache locations by timeStamp you can write in LocationManagerDidUpdateLocation
e.g
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let newLocation = locations.first {
let age: TimeInterval = -newLocation.timestamp.timeIntervalSinceNow
if age > 120 {
return
}
// ignore old (cached) updates
if newLocation.horizontalAccuracy < 0 {
return
}
}
}
I'm using CLLocationManager to get users location.
I want to get a single location update.
My problem is that I'm getting really bad horizontalAccuracy
location is %# <+xx.xxxxxx,+yy.yyyyyyy> +/- 3881.91m
verticalAccuracy: 65.4401861912846, horizontalAccuracy: 3881.90892434957
Code:
fileprivate lazy var locationManager: CLLocationManager = {
let manager = CLLocationManager()
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.delegate = self
manager.requestAlwaysAuthorization()
manager.pausesLocationUpdatesAutomatically = false
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.distanceFilter = kCLDistanceFilterNone
return manager
}()
override init() {
super.init()
locationManager.startUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let mostRecentLocation = locations.last else {
return
}
let verticalAccuracy = mostRecentLocation.verticalAccuracy
let horizontalAccuracy = mostRecentLocation.horizontalAccuracy
print("location is %#", mostRecentLocation)
print("verticalAccuracy: \(verticalAccuracy), horizontalAccuracy:\(horizontalAccuracy)")
}
Any suggestions why is this happening?
I'm in a room next to a window so i except to get bad accuracy but not that bad.
Thanks
I'm getting ridiculous results.
I got horizontalAccuracy of 15,000 m.
When i go out doors it works great but in doors should not be as bad as this.
Using Cellular triangulation and wifi should give a lot better results.
after 20 minutes i started to get good results of +- 50 m accuracy in doors.
I suggest you add a condition to make sure that you will just be using a location that have a better accuracy. On my case I use 20 as the desired horizontal accuracy on below example.
Example:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
locationManager.stopUpdatingLocation()
if locations.last!.horizontalAccuracy < 20 {
//Only use location that enters here
}
else {
//If the accuracy is not met then start updating location again and if possible increase more the accuracy (use kCLLocationAccuracyBestForNavigation if you really need it). Make sure that you use the desired accuracy and filter properly to avoid draining battery.
locationManager.startUpdatingLocation()
}
}
I always have found that when you first start location updates, the first few readings are quite poor, and then the accuracy improves as the GPS settles down.
What I do is to first check if the timestamp on the location is more than a few seconds old. If it is, I discard it. (Not sure if recent OS versions still send "stale" GPS readings, but the system used to give you a location from the last time the GPS was powered up, sometimes several hours old.)
Once I get current locations, I check the accuracy, and discard any location updates who's accuracy reading is too poor. Only when I get a reading that's good enough do I use it (and stop location updates in your case, since you only want 1 update.)
Bear in mind that it can take 30 seconds (or more) for the GPS to settle down, and in an area with poor GPS signal, you may never get a good enough reading.
I'm developing an app that monitors significant location changes to get the user's location in the background. I've successfully implemented locationManager.startMonitoringSignificantLocationChanges and the locationManager:didUpdateLocations and locationManager:didFailWithError methods of my CLLocationManagerDelegate.
However, SLC is actually more accurate than I need. According to Apple's docs - and corroborated by my tests - slc triggers a location update roughly every 500m and between 5 and 10 minutes. Therefore, I implemented locationManager.allowDeferredLocationUpdatesUntilTravelled:timeout in my delegate's didUpdateLocations method, as described in this guide: http://apple.co/1W4gqEJ.
Here's my code:
var deferringUpdates = false
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
for location in locations {
NSLog("Significant location change recorded:\n%#", location)
}
if let location = locations.first {
let secondsAgo: NSTimeInterval = location.timestamp.timeIntervalSinceNow
// Only process the location if it is very recent (less than 15 seconds old).
if abs(secondsAgo) < 15.0 {
saveExtendedUserInfo(withLocation: location)
}
}
if !deferringUpdates {
manager.allowDeferredLocationUpdatesUntilTraveled(810, timeout: 600)
deferringUpdates = true
NSLog("Deferring location updates...")
}
}
func locationManager(manager: CLLocationManager, didFailWithError error: NSError) {
NSLog(error.localizedDescription)
}
func locationManager(manager: CLLocationManager,
didFinishDeferredUpdatesWithError error: NSError?)
{
deferringUpdates = false
if let deferralError = error {
NSLog(deferralError.localizedDescription)
}
}
Unfortunately, the the location manager never defers the updates. Immediately after allowDeferredUpdatesUntilTravelled:timeout is called, the delegate executes didFinishDeferredUpdatesWithError and produces kCLErrorDomain 12, which is CLError.DeferredNotUpdatingLocation.
Why am I getting that error? It seems to mean that the deferred update service doesn't recognize monitoring significant location changes as "updating location". Is it possible to defer the delivery of slc events, or somehow reduce their frequency? If so, how?
The purpose of deferring updates is to save battery consumed by the main CPU when processing 1 Hz location updates from the GPS. With deferred updates the CPU stays asleep saving battery while the GPS chip accumulates GPS locations once per second (1 Hz).
With Significant Location Change (SLC) the system is not using the GPS. It is determining location based on cell tower triangulation, which does not wake up the CPU until a significant change has happened.
These two feature are mutually exclusive, you can't defer Significant Location Change's updates because the GPS chip is not involved in SLC.