Работа с геолокацией в iOS 24-7

в 5:14, , рубрики: CLLocationManager, CMMotionActivity, core location, ios development, swift, xcode, разработка мобильных приложений, разработка под iOS

Работа с геолокацией в iOS 24-7 - 1

В последнее время, часто вижу вопросы подобные этому:

Можно ли в iOS работать с геолокацией, когда приложение свернули и отправлять данные на сервер?

Это действительно возможно и совсем не сложно.
How to вместится в превью статьи.

Однако, зачастую задача более комплексная и поскольку у меня есть значительный опыт в данной области, я решил поделиться этим опытом.

Чтобы была какая-то конкретика, я предположил, что перед нами стоит задача написать вело-трекер. Со стороны пользователя это выглядит так:

  1. Скачиваю приложение
  2. Запускаю
  3. Регистрируюсь
  4. Нажимаю куда просят
  5. Закрываю
  6. Катаюсь
  7. Запускаю
  8. Вижу результат

p.s. финальный код здесь.

Для разработки можно выделить 3 основных направления работы.

  1. Стабильность работы с гео-данными.
    Пользователь запустил приложение, свернул или же вовсе закрыл — приложение должно обрабатывать данные.
  2. Экономия батареи.
    Пожалуй, не нуждается в дополнительном пояснении.
    Кстати, это самая сложная часть работы.
  3. Правильная обработка данных.
    Работу с данными нужно тестировать и отлаживать.

Инициализация менеджера

Для начала рассмотрим столь привычную работу с CLLocationManager.

  1. Создаем менеджер.
  2. Подписываемся на события.
  3. Запускаем менеджер для реакции на изменения геолокации.

Минимальный код

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 права на работу с геолокацией:

  1. requestWhenInUseAuthorization — можем получать информацию об обновлении локации, когда приложение активно.
  2. requestAlwaysAuthorization — дополнительно получаем возможность получения событий CLLocationManager API, когда приложение не активно / закрыто.

Из этого можно подумать, что работать с геолокацией в фоне можно только с правами requestAlwaysAuthorization — это не так.

И точно так же requestAlwaysAuthorization не позволяет спокойно работать в фоне "из коробки". Речь идет о работе с регионами, популярными местами, значительными перемещениями и тп.

Если код приведенный выше это что есть в проекте, то вызов метода requestWhenInUseAuthorization() либо requestAlwaysAuthorization() не покажет пользователю алерт о запросе прав.
Для этого так же необходимо добавить поясняющий текст сообщения в info.plist в соотвествующий ключ NSLocationAlwaysUsageDescription / NSLocationWhenInUseUsageDescription

Теперь, после подтверждения пользователем прав, мы можем работать с геолокацией.

Работаем в фоне

Чтобы приложение могло работать с геолокацией в фоне, необходимо сделать 2 вещи:

  1. Выставить у CLLocationManager
    allowsBackgroundLocationUpdates = true
  2. Добавить в параметр в Background Modes ("Background Modes -> Location updates"). В противном случае будет выброшен exception.

Все, приложение может работать в фоне с геолокацией, а также отправлять сетевые запросы и тп.

Что-то пошло не по плану

Как только пользователь, свернув приложение, будет некоторое время оставаться с точки зрения системы неподвижно, геолокация остановится, а вместе с ним и приложение.
Все дело в том, что CLLocationManager по умолчанию использует паузу для геолокации pausesLocationUpdatesAutomatically. И этот параметр не так прост, как кажется.

  1. Я запустил приложение и начал движение. Приложение работает.
  2. Я свернул приложение и продолжаю движение. Приложение работает.
  3. Я встретил друга и остановился с ним поговорить. Приложение все еще работает.

И в какой то момент оно перестает работать.

Как вы понимаете, включилась пауза, а через некоторое время остановилось работа и самого приложения находящееся в фоне. Довольно логично, ведь это пауза.

Я продолжил движение, но LocationManager все еще на паузе. И он будет оставаться на паузе, до тех самых пор, пока я сам не разверну приложение.

Таким образом, система старается экономить заряд батареи в случаях, когда нужно работать до "остановки".

p.s. если пауза нужна

Если все же приложению нужно работать до окончания движения, то можно помочь системе лучше определить это состояние указав в "activityType" подходящую цель, для чего мы работаем с геолокацией.

Для нас это ощутимая проблема, поэтому просто отключаем паузу для LocationManager'а

pausesLocationUpdatesAutomatically = false

Не убивай меня, Иван-царевич username!

Ранее, я уже упоминал о праве доступа к геолокации 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.

  • distanceFilter позволяет задать дистанцию в метрах, в течении которого нас не интересует изменение геолокации от точки до точки. Экономия не сказать что огромная, но экономия.
  • Отложенные уведомления (allowDeferredLocationUpdates) позволяют системе доставлять информацию по своему усмотрению, либо в соотвествии с заданными критериями.
    Критериями могут выступать дистанция и время.
    К примеру, с момента получения последней точки логика приложения не сломается, если в течении 5 минут точки могут не приходить, но потом система их доставит все разом.
    Критерием выступает время равное 5 минутам. Если другое приложение в это время запросит геоданные, система может попутно и нам отдать накопленные точки.

Работаем с данными тогда, когда это действительно нужно

Мы обозначили, что наша цель это вело-трекер. В данный момент мы обрабатываем данные всегда, не зависимо от того нужны они нам или нет. Из-за паузы мы не можем останавливать геолокацию.

Одно из решений будет менять требуемое качество данных во время поездки и в остальное время. Разница между наилучшими данными и наихудшими почти в 4 раза.

Изменения настроек CLLocationManager в зависимости от состояния

func setActiveMode(_ value: Bool) {
    if value {
        locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
        locationManager.distanceFilter = 10
        locationManager.disallowDeferredLocationUpdates()
    } else {
        locationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers
        locationManager.distanceFilter = CLLocationDistanceMax
        locationManager.allowDeferredLocationUpdates(untilTraveled: CLLocationDistanceMax,
                                                     timeout: CLTimeIntervalMax)
    }
}

Теперь осталось только отследить, когда пользователь едет на велосипеде. Для этого мы можем использовать CMMotionActivityManager из CoreMotion.

Отслеживаем тип активности

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

Полный код LocationService

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
            locationManager.disallowDeferredLocationUpdates()
        } else {
            locationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers
            locationManager.distanceFilter = CLLocationDistanceMax
            locationManager.allowDeferredLocationUpdates(untilTraveled: CLLocationDistanceMax,
                                                         timeout: CLTimeIntervalMax)
        }
    }

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    }
}

График энергопотребления vs часы работы.

Работа с геолокацией в iOS 24-7 - 2

Тип A: Максимальное качество
Тип B: Максимальное качество + фильтрация + отложенное получение
Тип C: Худшее качество + фильтрация + отложенное получение
Тип D: Без приложения

Можно ли еще улучшить? Разумеется. Данный подход необходим, если критично обрабатывать всю геолокацию в фоне. Дальше зависит от вашей фантазии.

Для iOS 10+ необходимо прописать NSMotionUsageDescription в Info.plist

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

Разрешаем в настройках схемы симуляцию геолокации и загружаем наш GPX файл.

Работа с геолокацией в iOS 24-7 - 3

Выбираем нужную симуляцию и пользуемся отладкой.

Работа с геолокацией в iOS 24-7 - 4

Вместо заключения

К сожалению, сложно написать что есть интересное, что есть очевидное, поэтому совсем не много моментов:

  • Если приложение "перезапустилось" в фоне, то пользователь не сможет его "убить".
  • Если геолокация ушла в паузу, когда приложение находится в фоне, то снять с паузы не получится, используя пуш-уведомления, регионы и тп. Тоже самое относится к перезапуску менеджера. Применится только после "разворота" приложения.
  • Геолокация работает с погрешностью. Если не использовать фильтр, то можно стоя на одном месте получить множество изменений геолокации.
  • Можно включать симуляцию геолокации на реальном девайсе, причем другие приложения так же будут работать с "новой" геолокацией. К примеру, карты.
  • При длительной симуляции геолокации реальный девайс может "залипнуть" и перестать отключать симуляцию. Помогает только перезагрузка.
  • При старте приложения не исключено, что вы получите "статую точку". Не забывайте отсматривать ts.
  • Можно сделать обертку над CLLocationManager и парсить GPX для тестирования.
  • GPX файл позволяет задавать скорость в точках.

p.s. финальный код здесь.

Автор: Мыльников Артем

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js