Привет, меня зовут Макс, я разрабатываю iOS-приложение Додо Пиццы. Наша фича-команда работает над улучшением опыта заказа через приложение в зале и тестирует гипотезы, связанные с геолокацией. Мы столкнулись с проблемой, что система не позволяет корректно отследить, какое разрешение даёт клиент при запросе геопозиции. Например, если он выбирает «Однократно», нет возможности отличить этот вариант от «При использовании».
Помимо этого, некоторые наши гипотезы тесно связаны с работой геолокации в бэкграунд-режиме. В нём резко повышается сложность работы: появляются новые системные алерты; состояния, при которых эти алерты показываются; варианты авторизации, которые может выбрать клиент.
Непонимание всех тонкостей авторизации и работы геолокации в бэкграунде привели нас к необходимости детально погрузиться в тему. Результатами исследований делюсь в сегодняшней статье.
Что умеет геолокация на iOS в бэкграунд-режиме
Работа с геолокацией в состоянии, когда приложение открыто, обычно не вызывает трудностей. Сейчас мы используем эту возможность для показа ближайшей пиццерии. Гораздо больший интерес и сложность представляет работа с геолокацией в бэкграунд-режиме. Эти возможности можно поделить на две группы:
1. Непрерывное отслеживание геопозиции после сворачивания приложения. Например, клиент делает заказ на пути в ресторан, сворачивает приложение. Мы понимаем, что клиент придет через 15 минут, поэтому начинаем готовить ему пиццу. Пока приложение открыто, подписываемся на изменения геопозиции. Затем продолжаем отслеживание в бэкграунд-режиме, т.е. после того, как пользователь свернул приложение. Можем установить частоту в метрах и желаемую точность.
var locationManager = CLLocationManager()
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.distanceFilter = 7
Далее устанавливаем Capabilities — Background Mode:
В коде необходимо выставить
locationManager.allowsBackgroundLocationUpdates = true
Обязательно нужно отключить автоматическую паузу, иначе система может остановить трекинг в бэкграунде и запустить его больше не удастся:
locationManager.pausesLocationUpdatesAutomatically = false
Запускаем отслеживание геопозиции:
locationManager.startUpdatingLocation()
После сворачивания приложения система покажет индикацию в виде синей стрелки в статус-баре (для устройств без чёлки статус-бар станет синим).
Приложение будет отслеживать геопозицию в запущенном состоянии до тех пор, пока мы не остановим отслеживание, либо пока пользователь не закроет приложение принудительно.
Для данного способа достаточно авторизации When In Use. При авторизации с типом Always система не будет показывать синюю индикацию. Подробнее про авторизацию поговорим далее.
2. Получение геопозиции при возникновении событий смены локации. Например, когда клиент проходит недалеко от пиццерии, он получает пуш-уведомление об актуальных акциях именно в ней.
Этот способ умеет запускать приложение, даже если оно было выгружено из памяти. После запуска мы можем получить текущую геолокацию. Для этого мы должны подписаться на некоторые события, находясь в запущенном состоянии.
К таким событиям относятся:
Significant location changes — значительные изменение геопозиции c точностью от 500 метров;
Visits — посещение популярного места: дом, работа, спортзал и т.д.;
Regions — посещение определённой геозоны, которую задает разработчик.
Для данного способа требуется авторизация типа Always.
Разбираемся в статусах и состояниях авторизации
В CoreLocation есть 2 запроса для разрешения на использование геолокации:
И 5 возможных статусов авторизации:
public enum CLAuthorizationStatus : Int32 {
case notDetermined = 0
case restricted = 1
case denied = 2
case authorizedAlways = 3
case authorizedWhenInUse = 4
}
В действительности состояний авторизации больше, чем статусов, а переходы между ними кажутся запутанными. Распутываем авторизацию в следующей схеме:
Значение переменной authorizationStatus
:
для зеленых блоков —
authorizedAlways
;для синих —
authorizedWhenInUse
.
На схеме есть два состояния, которые никак не отражены в статусе authorizationStatus
: When In Use Once и Provisional Always. Рассмотрим их поближе.
When In Use Once
Это состояние во многом похоже на When In Use, оно имеет такой же статус authorizedWhenInUse
. Но у When In Use Once есть два больших отличия:
после перезапуска или по истечению таймаута статус авторизации будет переключён в notDetermined
из этого состояния нельзя запросить разрешение типа Always, при вызове requestAlwaysAuthorization() ничего не произойдёт.
Provisional Always
Из схемы видно, что самое труднодоступное состояние — Always. Пользователь должен дважды увидеть алерт и оба раза дать согласие. Provisional always хоть и имеет статус authorizedAlways
, но по своим возможностям эквивалентен When In Use. Для перехода в состояние Provisional Always нужно вызвать requestAlwaysAuthorization() и выбрать «При использовании». Система покажет такой же алерт, как и при вызове requestWhenInUseAuthorization():
В будущем пользователь увидит алерт, в котором система предложит либо переключиться на Always, либо оставить на When In Use. В документации говорится, что это произойдёт в момент, когда система попытается запустить приложение при наступлении одного из событий (например, пользователь зашёл в регион). Как правило, это происходит, когда пользователь находится на главном экране iPhone.
The second prompt displays when Core Location prepares to deliver an event to your app requiring CLAuthorizationStatus.authorizedAlways.
Такое поведение больше похоже на неожиданное спам-сообщение. Пользователь уже давно ушёл из контекста нашего приложения. Велика вероятность, что он переключится на When In Use.
Более надёжный способ перейти в состояние Always: сначала получить от пользователя разрешение When In Use и только потом запрашивать Always. Можно попытаться сделать это сразу, последовательно вызывая requestWhenInUseAuthorization и requestAlwaysAuthorization().
Ещё один вариант гарантированно получить Always: сразу отправлять пользователя в настройки. Так, например, делает Telegram.
При активном использовании способа Always iOS покажет пользователю алерт, в котором предложит переключиться обратно на When In Use, либо остаться в Always.
Как можно заметить, схема авторизации в iOS сильно затрудняет сбор данных о том, насколько пользователь доверяет приложению и какой способ авторизации выбирает — та самая проблема, о которой шла речь в начале статьи.
Смена статуса авторизации: новые возможности в iOS 15
В iOS 15 появился фреймворк CoreLocationUI. В Apple стремятся упростить опыт пользователя в сценариях, когда необходимо разово получить геопозицию. Теперь можно использовать специальную кнопку CLLocationButton (LocationButton в SwiftUI), при нажатии на которую система сама спросит у пользователя разрешение, если это требуется.
Если текущий статус авторизации позволяет хоть какое-то использование геолокации (authorizedAlways
, authorizedWhenInUse
), то при нажатии на кнопку ничего не произойдёт. В противном случае логика смены статусов авторизации выглядит так, как показано на схеме:
Если пользователь однажды согласился дать разрешение на разовое использование геопозиции, то в другие сессии при нажатии на кнопку приложение будет сразу получать разрешение When In Use Once, без алертов.
Самая интересная фишка здесь — это возможность скинуть состояние авторизации в неопределённое (notDetermined), даже если ранее пользователь запретил использование геолокации.
Простой тест это подтверждает. Выполняем requestWhenInUseAuthorization(), запрещаем использование геолокации. Далее нажимаем на CLLocationButton, разрешаем разовое использование (нажимаем ОК). Перезапускаем приложение, чтобы статус авторизации cбросился в notDetermined. Снова выполняем requestWhenInUseAuthorization(), появляется алерт.
В предыдущих версиях повторный показ алерта был возможен только в случае, если пользователь сам перейдёт в настройки приложения и вручную сбросит запрет на использование геолокации. Таким образом, новый фреймворк даёт разработчикам возможность восстановить (возможно, навсегда потерянный) доступ приложения к геолокации. Задача разработчика — предоставить пользователю новую ценность при запросе авторизации, чтобы избежать повторного запрета.
Новые возможности в iOS 15 потенциально сокращают количество надоедливых алертов, а разработчикам не нужно писать обработку различных сценариев авторизации. Подробнее здесь: Meet the Location Button WWDC 2021.
Непрерывное отслеживание геопозиции при наступлении события
Допустим, мы хотим узнать скользящее значение скорости пользователя в момент, когда он зашёл в определённый регион. При этом приложение может быть выгружено из памяти.
Про скользящее
Значение скорости из CLLocation (location.speed) зачастую неточное. В качестве средней скорости можно использовать скользящее значение, например скорость за последние 50 метров.
Авторизуем геолокацию с типом Always. Далее подписываемся на событие входа в регион:
locationManager.startMonitoring(for: pizzeriaRegion)
И при получении события начинаем трекать геолокацию, как в способе 1:
func locationManager(
_ manager: CLLocationManager,
didEnterRegion region: CLRegion
) {
manager.startUpdatingLocation()
}
Я провёл несколько тестов на устройстве. В момент посещения региона приложение запускается, но система довольно быстро переводит его в состояние suspended, несмотря на то, что мы сконфигурировали и запустили CLLocationManager для подробного отслеживания геопозиции. Продолжать отслеживать геопоцизию непрерывно и рассчитывать скорость не удастся.
Коротко про особенности работы в бэкграунд-режиме
Непрерывное отслеживание геолокации:
может быть запущено только в состоянии foreground и продолжать работу после сворачивания приложения;
пользователь всегда понимает, что геопозиция отслеживается — синяя индикация в статус-баре;
достаточно авторизации When In Use, но при Always исчезает синяя индикация.
Примеры приложений: приложения такси, навигаторы, фитнес-трекеры.
Разовое получение геолокации при наступлении события:
ограниченный набор событий, при наступлении которых можно получить геолокацию;
система разбудит приложение при наступлении события, даже если оно выгружено из памяти;
требует Always авторизации. Сложно получить и сохранить разрешение от пользователя;
событие не придёт, если пользователь принудительно закрыл приложение.
Примеры приложений: Telegram (функционал «поделиться геопозицией»).