📜 ⬆️ ⬇️

Work with geolocation in iOS 24/7




Recently, I often see the question:
Is it possible to work with geolocation in iOS when the application is minimized and send data to the server?

It really is possible and not at all difficult.
How to fit into the preview of the article.

However, the task is often more complex and since I have significant experience in this field, I decided to share this experience.


In order to have some specifics, I assumed that the task before us was to write a cycle tracker. From the user's side, it looks like this:


  1. Download the application
  2. I'm running
  3. Register
  4. I click where they ask
  5. I close
  6. I ride
  7. I'm running
  8. I see the result

ps final code here .


For the development can be divided into 3 main areas of work.


  1. Stability of work with geo-data.
    The user has started the application, turned it off or closed it altogether - the application must process the data.
  2. Saving battery.
    Perhaps, it does not need additional explanation.
    By the way, this is the hardest part of the job.
  3. Correct data handling.
    Work with data needs to be tested and debugged.

Manager initialization


To begin, consider the usual work with CLLocationManager .


  1. Create a manager.
  2. Subscribe to events.
  3. Run the manager to respond to changes in geolocation.

Minimum code
import CoreLocation final class LocationService: NSObject, CLLocationManagerDelegate { private let locationManager = CLLocationManager() override init() { super.init() locationManager.delegate = self locationManager.startUpdatingLocation() } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { } } 

But for this to work, you need to confirm the user that he allows the use of information about his location.


... or have the right?


iOS provides 2 rights to work with geolocation:


  1. requestWhenInUseAuthorization - we can get information about the location update when the application is active.
  2. requestAlwaysAuthorization - in addition, we get the ability to receive CLLocationManager API events when the application is not active / closed.

From this you might think that working with geolocation in the background can only be done with requestAlwaysAuthorization rights - this is not so.


And in the same way, requestAlwaysAuthorization does not allow to work quietly in the background out of the box. It is about working with regions, popular places, significant movements and so on.


If the code above is what is in the project, then calling the requestWhenInUseAuthorization () method or requestAlwaysAuthorization () will not show the user a request for rights.
To do this, you also need to add explanatory text of the message to info.plist in the appropriate NSLocationAlwaysUsageDescription / NSLocationWhenInUseUsageDescription key


Now, after confirming the user rights, we can work with geolocation.


We work in the background


For an application to work with geolocation in the background, you need to do 2 things:


  1. Expose at CLLocationManager
     allowsBackgroundLocationUpdates = true 
  2. Add to the setting in Background Modes ("Background Modes -> Location updates"). Otherwise, exception will be thrown.

Everything, the application can work in the background with geolocation, as well as send network requests and so on.


Something went wrong


As soon as the user, having minimized the application, will remain stationary for some time from the point of view of the system, geolocation will stop, and with it the application.
The thing is that by default CLLocationManager uses a pause for geolocation pausesLocationUpdatesAutomatically . And this option is not as simple as it seems.


  1. I launched the application and started the movement. The application is working.
  2. I turned off the app and keep moving. The application is working.
  3. I met a friend and stopped to talk to him. The application is still working.

And at some point it stops working.


As you understand, a pause started, and after a while the work of the application itself, which is in the background, stopped. Pretty logical, because it is a pause.


I continued driving, but the LocationManager is still on pause. And it will remain on a pause, as long as I do not deploy the application myself.


Thus, the system tries to save battery power in cases when it is necessary to work before the “stop”.


ps if you need a pause

If, nevertheless, the application needs to work before the end of the movement, then it is possible to help the system better determine this state by specifying a suitable goal in the "activityType", for which we work with geolocation.


For us, this is a tangible problem, so we simply disable the pause for the LocationManager.


 pausesLocationUpdatesAutomatically = false 

Do not kill me, Ivan Tsarevich username!


Earlier, I mentioned the access rights to geolocation requestAlwaysAuthorization . And that this makes it possible to receive events of the CLLocationManager API . And to receive both being in the background, and in the unloaded state. In the case of the latter, the system can restart our application to deliver a new event. For example:


 locationManager.startMonitoringSignificantLocationChanges() —    locationManager.startMonitoringVisits() —    locationManager.startMonitoring(for: CLRegion) —         

This is what we will use. If the user is killing the application, then we need to get back to work as quickly as possible. In my case, the most suitable would be startMonitoringSignificantLocationChanges , since the regions have limitations in radius. The main thing is not to forget to start again to configure and run CLLocationManager .


Full code
 import CoreLocation final class LocationService: NSObject, CLLocationManagerDelegate { private let locationManager = CLLocationManager() override init() { super.init() locationManager.delegate = self locationManager.requestAlwaysAuthorization() locationManager.allowsBackgroundLocationUpdates = true locationManager.pausesLocationUpdatesAutomatically = false locationManager.startUpdatingLocation() locationManager.startMonitoringSignificantLocationChanges() } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { } } 

Fine! The application works in the background, the application is restarted, works with geolocation, can work with the network, and so. That's just to remove us with such activity from the device.


Battery saving


If you open the statistics of energy consumption, then our application with a probability of 99.99% will be the leader and unfortunately not in terms of savings. Therefore, we will now optimize.


Error


The battery consumption is greatly affected by the required error from the CLLocationManager .
We can demand the most accurate data, but we can, with an error of about 10 meters, 3 kilometers, and so on ( kCLLocationAccuracy * ).
Appropriately, the higher the required data quality, the greater the battery consumption.


Therefore, when the error of 100m is enough for you, you do not need to take the maximum quality.
More interestingly, if you demand poor quality, then most likely the system will produce more than you expect. Therefore, it is imperative not to require an error better than you really need.
ps required does not mean valid.


Configuration


Additionally, you can win in the fight for the battery, if you remember about distanceFilter and allowDeferredLocationUpdates .



We work with data when you really need it.


We have indicated that our goal is a bicycle tracker. At the moment we are processing data always, regardless of whether we need it or not. Because of the pause, we can not stop geolocation.


One of the solutions will change the required data quality during the trip and at other times. The difference between the best and worst data is almost 4 times.


Changes to CLLocationManager settings depending on the state
 func setActiveMode(_ value: Bool) { if value { locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters locationManager.distanceFilter = 10 } else { locationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers locationManager.distanceFilter = CLLocationDistanceMax } } 

Now it remains only to track when the user rides a bicycle. For this we can use CMMotionActivityManager from CoreMotion .


We track the type of activity
 motionManager.startActivityUpdates(to: .main, withHandler: { [weak self] activity in self?.setActiveMode(activity?.cycling ?? false) }) 

Full LocationService Code
 import CoreLocation import CoreMotion final class LocationService: NSObject, CLLocationManagerDelegate { private let locationManager = CLLocationManager() private let motionManager = CMMotionActivityManager() override init() { super.init() locationManager.delegate = self locationManager.requestAlwaysAuthorization() locationManager.allowsBackgroundLocationUpdates = true locationManager.pausesLocationUpdatesAutomatically = false setActiveMode(true) locationManager.startUpdatingLocation() locationManager.startMonitoringSignificantLocationChanges() motionManager.startActivityUpdates(to: .main, withHandler: { [weak self] activity in self?.setActiveMode(activity?.cycling ?? false) }) } func setActiveMode(_ value: Bool) { if value { locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters locationManager.distanceFilter = 10 } else { locationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers locationManager.distanceFilter = CLLocationDistanceMax } } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { } } 

Energy schedule vs hours of operation.

Type A: Maximum Quality
Type B: Maximum Quality + Filtration
Type C: Poor quality + filtering
Type D: Without Application


Is it possible to further improve? Of course. This approach is necessary if it is critical to process all geolocation in the background. Further depends on your imagination.


For iOS 10+, you need to register NSMotionUsageDescription in Info.plist
 <key>NSMotionUsageDescription</key> <string>$(PRODUCT_NAME) motion use.</string> 

Checking geodata handling


The application must be checked. Getting off the writing process for a “trip” is not a good idea. Yes, and about debugging in this approach, you can forget.


Fortunately, Apple allows us to use GPX files (and we don’t even need a real device to work in this case).


We select the service that generates the route of the movement, and save the following file in gpx:


Sample file
 <?xml version="1.0"?> <gpx version="1.1" creator="Xcode"> <wpt lat="54.91148" lon="83.07381"/> <wpt lat="54.90792" lon="83.07243"/> </gpx> 

We allow geolocation simulation in the scheme settings and load our GPX file.

Select the desired simulation and use debugging.

Instead of conclusion


Unfortunately, it is difficult to write what is interesting, what is obvious, so there are not many things to do:



ps final code here .


')

Source: https://habr.com/ru/post/275769/


All Articles