GPS Accuracy Resetting on its own on iOS - ios

In an iPhone app that I'm building, GPS accuracy is a user-controlled value that can be set to either KCLLocationAccuracyNearestTenMeters or KCLLocationAccuracyHundred Meters. The code works fine to capture a track, but when monitoring the app in the Location Energy Impact Instrument I’m seeing some unexpected behavior. When accuracy is set to nearest 10 meters the app works as expected, however, if it’s set to 100 meters the setting remains OK for between a half and two seconds, as shown by the Instrument, but then it switches to KCLLocationAccuracy Best. This setting isn't an option in the app, or even a String anywhere in the code.
One twist here, I am running this as Swift 3.0 using Xcode 8, but it's working as it did before and the new platform doesn't seem to be an issue (and shouldn't have this kind of impact). It does mean that some API's have changed and may look different (and to my taste, better) below.
Obviously, this resetting has a very counterproductive impact on energy consumption, which shows in the Instrument which goes from Low to High energy usage at the point where it switches. I can't identify any point where the app would be doing this unintentionally - in fact, during this one-second period it should only be appending points returned by didUpdateLocations to a pending buffer for later processing, as shown below.
In a shared constants declaration:
let defaultTrackingAccuracy = kCLLocationAccuracyNearestTenMeters
let alternateTrackingAccuracy = kCLLocationAccuracyHundredMeters
var trackingAccuracy = kCLLocationAccuracyNearestTenMeters
var waypointInterval = 100
In a settings ViewController (can also reset waypointInterval but did not):
#IBAction func accuracySwitched(_ sender: UISwitch) {
if accuracySwitch.isOn {
Set.shared.trackingAccuracy = Set.shared.defaultTrackingAccuracy
} else {
Set.shared.trackingAccuracy = Set.shared.alternateTrackingAccuracy
}
}
LocationManager instantiation in a Model portion of code:
lazy var locationManager: CLLocationManager = {
var _locationManager = CLLocationManager()
_locationManager.delegate = self
_locationManager.desiredAccuracy = Set.shared.trackingAccuracy
_locationManager.allowsBackgroundLocationUpdates = true
_locationManager.distanceFilter = Double(Set.shared.waypointInterval)
return _locationManager
}()
Starting the location manager updates in the Model:
func setupLocationManager()
{
if CLLocationManager.authorizationStatus() != .authorizedAlways {
locationManager.requestAlwaysAuthorization()
if CLLocationManager.authorizationStatus() != .authorizedAlways {
delegate?.displayNotice("Unable to Capture Track", alertMessage: "This device requires authorization to use location services in order to capture a track. \n\nPress Continue to return to the Track List.", buttonText: "Continue")
}
}
if !CLLocationManager.locationServicesEnabled() {
delegate?.abortTrackCapture(.gpsUnavailable)
}
locationManager.startUpdatingLocation()
}
Processing points returned by the location manager instance, in the same Model portion:
#objc(locationManager:didUpdateLocations:) func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
{
assert(locations.first != nil, "didUpdateLocations called with empty array.")
if recordStatus == .record {
pendingPointsBuffer = pendingPointsBuffer + locations
}
lastPosition.latitude = locations.last!.coordinate.latitude
lastPosition.longitude = locations.last!.coordinate.longitude
lastPosition.elevation = locations.last!.altitude
}
Every few seconds, there is a routine that takes points from the buffer and posts them into a CoreData database, but that only accesses the buffer - it has no interaction with the location manager (so it can sleep through periods when the app is in background while the code above keeps loading points into the buffer).
The user interface is in a separate ViewController module, but shouldn’t be doing anything during the two seconds in question.
I'm thinking that there may be some OS based setting or process that could be doing this, but I haven't found anything that seems to do that.
Thanks for your input - any thoughts are appreciated.

Related

Background location service stops working in a period of time

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

CLLocationManager not updating while in background, and get messy location coordinates Swift

I have a tracking function but it doesn't update location while in background.
1st case: Tracking while app is in the foreground -> the tracking is actually happening but doesn't get precise coordinates. I will change to locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation to see if improves accuracy of the tracking.
2nd case: Tracking while screen is off -> the tracking is a straight line from a to b, tracking doesn't update coordinates.
3rd case: Tracking while app is in back ground(pressed home button) -> tracking is happening as case 1.
I found a post that explains that if authorisation is set to always you have to specify you want to keep updating location while in background, but nothing has changed. This is the code and info.plist :
override func viewDidLoad() {
super.viewDidLoad()
mapView.delegate = self
locationManager.delegate = self
// locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
// locationManager.allowsBackgroundLocationUpdates = true //for getting user location in background mode as well
mapView.showsUserLocation = true
mapView.userTrackingMode = .follow //map following user
configureLocationServices()
addDoubleTap() // enabling duble tap gesture recognizer
// mapView.isUserInteractionEnabled = true
let location = locationManager.location?.coordinate
let region = MKCoordinateRegionMakeWithDistance(location!, 1000, 1000) // set mapView based on user location coordinates
mapView.setRegion(region, animated: true)
centerMapOnLocation()
// alerts coordinates to post to Firebase
let alertDrawLatitude = alertDrawCoordinates?.latitude // not used ?
let alertDrawLomgitude = alertDrawCoordinates?.longitude
let title: String? = alertNotificationType
var subtitle: String? = alertNotificationType
// user alert notification. takes coordinates from alertNotificationArray( populated with firebase returning coordinate for all alerts
displayAlerts()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let mostRecentLocation = locations.last else { return }
self.actualRouteInUseCoordinatesArray.append(mostRecentLocation.coordinate)
}
func configureLocationServices() {
if authorizationStatus == .notDetermined{
locationManager.requestAlwaysAuthorization()
} else if authorizationStatus == .authorizedAlways {
locationManager.showsBackgroundLocationIndicator = true //set update location even if in background. very imposrtant!!
}
}
UPDATE:
changing the accuracy only made things worse.
with AccuracyBest:
and with AccuracyBestForNAvigation
second tracking is actually worse.. how can navigation apps rely on this kind of tracking? is there anything wrong with my code for LocationManager?
SECOND UPDATE:
it now get updated location when in background, but is way off..I never passed the yellow street and it shows like I waked for 10 minutes after it..
THIRD EDIT:
I found out that I should filter out GPS raw data, so I'm using a Kalman filter, and it really smooths out the resulting tracking.
So I'm fine tuning two parameters, and in order to be able to change those parameters I added two textfields #IBOutlet weak var filterValueTextField: UITextField! and #IBOutlet weak var horizontalAccuracyTextField: UITextField!and connected those to the parameters
hcKalmanFilter?.rValue = Double(String( describing:filterValueTextField?.text!))! and guard mostRecentLocation.horizontalAccuracy < Double(String( describing: horizontalAccuracyTextField?.text!))! else { return }.
My problem is now that it finds nil while unwrapping value in the horizontalAccuracy parameter.
If in horizontalAccuracy I just put a value it accepts an integer, but when I take it from the texField converting the textfield.text to Int, compiler throws an error Binary operator '<' cannot be applied to operands of type 'CLLocationAccuracy' (aka 'Double') and 'Int', while if I convert it to Double doesn't, but it finds nil.
Why the filterValue finds a value from it's textField, and the horizontal Accuracy doesn't? they're declared, and use the same way.
Any idea?
First of all you have limited time while your app goes to background, and that time is depend upon load on your device's OS, but most probably it is approx. 30 seconds. So this is the reason your are not getting location updates while your screen is off or while your app goes to background.
But Apple allows app to run in background for some tasks and location update is one of them, so you can fetch location updates even if your app goes to background by enabling Background Fetch capability for your app.
For more details please follow below official doc. of Apple:
https://developer.apple.com/documentation/corelocation/getting_the_user_s_location/handling_location_events_in_the_background
And secondly try to maintain your locationmanager object in global scope of your app like you can place it in AppDelegate or in Singleton class if you are maintaining any for your app, so it will always be available.
Sometimes location that you receive does not have desired accuracy, especially when you've just started tracking, first couple of locations are going to be well off. You can use location's horizontal accuracy property to filter location witch have, for example, less then 50m accuracy

Keeping track of time in background to handle location updates

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

Getting really bad accuracy from CLLocationManager

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.

didUpdateLocations always be called several times with different coordinates

So, my app requires to capture location every 15 meter.
in viewDidLoad
NSOperationQueue.mainQueue().addOperationWithBlock {
self.manager = CLLocationManager()
self.manager.delegate = self
self.manager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
self.manager.distanceFilter = 15.0
self.manager.headingFilter = 10
self.manager.startUpdatingLocation()
}
First time (i don't move), in
func locationManager(manager:CLLocationManager, didUpdateLocations locations:[AnyObject])
i get (real example):
*.1488241293991
*.5997807433053
second time:
*.1489010891664
*.599624152471
I test on real device with ios7, and swift.
What am I doing wrong?
Thanks!
From CLLocationManager Class Reference:
When requesting high-accuracy location data, the initial event delivered by the location service may not have the accuracy you requested. The location service delivers the initial event as quickly as possible. It then continues to determine the location with the accuracy you requested and delivers additional events, as necessary, when that data is available.
So the first location you retrieve might be inaccurate.

Resources