В последнее время, часто вижу вопрос:
Можно ли в iOS работать с геолокацией, когда приложение свернули и отправлять данные на сервер?
Однако, зачастую задача более комплексная и поскольку у меня есть значительный опыт в данной области, я решил поделиться этим опытом.
Чтобы была какая-то конкретика, я предположил, что перед нами стоит задача написать вело-трекер. Со стороны пользователя это выглядит так:
p.s. финальный код здесь.
Для разработки можно выделить 3 основных направления работы.
Для начала рассмотрим столь привычную работу с CLLocationManager.
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]) {
}
}
Но чтобы это заработало, необходимо подтверждение пользователя, что он позволяет использовать информацию о его местоположении.
iOS предоставляет 2 права на работу с геолокацией:
Из этого можно подумать, что работать с геолокацией в фоне можно только с правами requestAlwaysAuthorization — это не так.
И точно так же requestAlwaysAuthorization не позволяет спокойно работать в фоне "из коробки". Речь идет о работе с регионами, популярными местами, значительными перемещениями и тп.
Если код приведенный выше это что есть в проекте, то вызов метода requestWhenInUseAuthorization() либо requestAlwaysAuthorization() не покажет пользователю алерт о запросе прав.
Для этого так же необходимо добавить поясняющий текст сообщения в info.plist в соотвествующий ключ NSLocationAlwaysUsageDescription / NSLocationWhenInUseUsageDescription
Теперь, после подтверждения пользователем прав, мы можем работать с геолокацией.
Чтобы приложение могло работать с геолокацией в фоне, необходимо сделать 2 вещи:
allowsBackgroundLocationUpdates = true
Все, приложение может работать в фоне с геолокацией, а также отправлять сетевые запросы и тп.
Как только пользователь, свернув приложение, будет некоторое время оставаться с точки зрения системы неподвижно, геолокация остановится, а вместе с ним и приложение.
Все дело в том, что CLLocationManager по умолчанию использует паузу для геолокации pausesLocationUpdatesAutomatically. И этот параметр не так прост, как кажется.
И в какой то момент оно перестает работать.
Как вы понимаете, включилась пауза, а через некоторое время остановилось работа и самого приложения находящееся в фоне. Довольно логично, ведь это пауза.
Я продолжил движение, но LocationManager все еще на паузе. И он будет оставаться на паузе, до тех самых пор, пока я сам не разверну приложение.
Таким образом, система старается экономить заряд батареи в случаях, когда нужно работать до "остановки".
Если все же приложению нужно работать до окончания движения, то можно помочь системе лучше определить это состояние указав в "activityType" подходящую цель, для чего мы работаем с геолокацией.
Для нас это ощутимая проблема, поэтому просто отключаем паузу для LocationManager'а
pausesLocationUpdatesAutomatically = false
Ранее, я уже упоминал о праве доступа к геолокации requestAlwaysAuthorization. И о том, что это дает возможность получать события CLLocationManager API. Причем получать как находясь в фоне, так и в выгруженном состоянии. В случае последнего, система может перезапустить наше приложение, чтобы доставить новое событие. К примеру:
locationManager.startMonitoringSignificantLocationChanges() — на значительные перемещения
locationManager.startMonitoringVisits() — регулярно посещаемые места
locationManager.startMonitoring(for: CLRegion) — а также вход или выход из установленной области
Это мы и будем использовать. Если пользовать убивает приложение, то нам нужно максимально быстро вернуться в работу. В моем случае самое подходящее будет startMonitoringSignificantLocationChanges, поскольку регионы имеют ограничения в радиусе. Главное не забыть по запуску опять настроить и запустить CLLocationManager.
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]) {
}
}
Отлично! Приложение работает в фоне, приложение перезапускается, работает с геолокацией, может работать с сетью и тп. Вот только удалят нас с такой активностью с девайса.
Если открыть статистику энергопотребления, то наше приложение с вероятностью 99.99% будет лидером и к сожалению не по экономии. Поэтому теперь будем оптимизировать.
На расход батареи очень сильно влияет требуемая погрешность от CLLocationManager.
Мы можем потребовать максимально точные данные, а можем с погрешностью около 10 метров, 3 километра и тп (kCLLocationAccuracy*).
Соотвественно, чем выше требуемое качество данных, тем больше расход батареи.
Поэтому, когда вам достаточно погрешности в 100м, не нужно брать максимальное качество.
Более интересно то, что если требовать низкое качество, то скорее всего система даст больше, чем вы ожидаете. Поэтому крайне важно не требовать погрешность лучше, чем вам действительно нужно.
p.s. требуемое не означает действительное.
Дополнительно можно выиграть в борьбе за батарею, если вспомнить о distanceFilter и allowDeferredLocationUpdates.
Мы обозначили, что наша цель это вело-трекер. В данный момент мы обрабатываем данные всегда, не зависимо от того нужны они нам или нет. Из-за паузы мы не можем останавливать геолокацию.
Одно из решений будет менять требуемое качество данных во время поездки и в остальное время. Разница между наилучшими данными и наихудшими почти в 4 раза.
func setActiveMode(_ value: Bool) {
if value {
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
locationManager.distanceFilter = 10
} else {
locationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers
locationManager.distanceFilter = CLLocationDistanceMax
}
}
Теперь осталось только отследить, когда пользователь едет на велосипеде. Для этого мы можем использовать CMMotionActivityManager из CoreMotion.
motionManager.startActivityUpdates(to: .main, withHandler: { [weak self] activity in
self?.setActiveMode(activity?.cycling ?? false)
})
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]) {
}
}
Тип A: Максимальное качество
Тип B: Максимальное качество + фильтрация
Тип C: Худшее качество + фильтрация
Тип D: Без приложения
Можно ли еще улучшить? Разумеется. Данный подход необходим, если критично обрабатывать всю геолокацию в фоне. Дальше зависит от вашей фантазии.
<key>NSMotionUsageDescription</key>
<string>$(PRODUCT_NAME) motion use.</string>
Работу приложения надо проверять. Отрываться в процессе написания на "поездку" — не самая хорошая идея. Да и про дебагинг в таком подходе можно забыть.
К счастью, Apple позволяет нам использовать GPX файлы (и нам даже не нужен реальный девайс для работы в данным случае).
Выбираем сервис, генерирующий маршрут движения, и сохраняем в gpx файл вида:
<?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>
К сожалению, сложно написать что есть интересное, что есть очевидное, поэтому совсем не много моментов:
p.s. финальный код здесь.
К сожалению, не доступен сервер mySQL