CoreLocation / 1.1. CoreLocation



"Programming today is a race between software engineers striving to build bigger and better idiot-proof programs, and the universe trying to build bigger and better idiots. So far, the universe is winning."

Rick Cook

В этом уроке


Trivia

Часто нам приходится пользоваться приложениями, позволяющими отслеживать нашу геопозицию - то есть положение в реальном времени. Современные устройства оборудованы множеством приспособлений для выяснения позиции:

Часто бывает полезным выполнить какое-то действие в зависимости от позиции или же просто считать данные для понимания происходящего (например, число шагов).


Скелет приложения

  1. Создайте приложение для iOS типа Single View.
  2. Добавьте наши ассеты
  3. Разместите на главном полотне картинку:
    • Нулевые отступы от сторон родительского вида
    • Aspect Fill, что позволит заполнять картинке вид, не теряя размеров
    • Clip to Bounds, срежем лишние части картинки за границами представления
    • Сама картинка background
    • Аутлет backgroundImage

В прошлом мы создавали красивый эффект блюра для картинки. Давайте повторим это, только сделаем его эффектом Vibrancy (Следующий код помещается внутрь viewDidLoad):

let blurEffectView: UIVisualEffectView = {
    //Обычное представление для блюра
    let blurEffect = UIBlurEffect(style:  .light)
    let blurEffectView = UIVisualEffectView(effect: blurEffect)

    //Мешаем правилам авто-изменения размеров превращаться в ограничения
    blurEffectView.translatesAutoresizingMaskIntoConstraints = false

    //Создаём эффект размытия и добавляем в блюр
    let vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect)
    let vibrancyView = UIVisualEffectView(effect: vibrancyEffect)
    blurEffectView.contentView.addSubview(vibrancyView)
    return blurEffectView
}()
blurEffectView.frame = backgroundImage.frame
backgroundImage.addSubview(blurEffectView)

Новинкой здесь может стать, что мы создаём наше представление с помощью замыкания, а не напрямую. Также как со свойствами, это позволяет сконфигурировать объект прямо в момент создания.

Запустите приложение и Вы получите


Геолокация

За определение позиции отвечает Фреймворк Core Location. Импортируйте его в контроллер:

import CoreLocation

Так как определение геопозиции - крайне дорогая по затратам энергии операция, то существует несколько вариантов для определения позиции:

В случае отсутствия какого-то из компонент геолокации все остальные сервисы должны работать в приложении, если геопозиция лишь дополнительное действие в приложении, а не основное. Так дневник должен позволять делать записи, даже если не сможет подсказать Вам, где Вы их сделали. (Если только Вашей целью не является сбор данных о Ваших клиентах.)

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

Для работы нужно добавить следующие ключи в Info.plist с описанием того, для чего требуется то или иное действие.

//Для базовых сервисов
Privacy - Location When In Use Usage Description
//Для постоянного отслеживания
Privacy - Location Always and When In Use Usage Description

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

Учтите, что первый метод не даст Вам отслеживать перемещения по регионам, существенные изменения положения и список мест за день.

Учтите, что не во всех случаях сервисы геопозиции могут работать:

Выделяют 4 основных метода для отслеживания;


Отслеживание изменений

Чтобы получать данные об обновлении позиции сперва следует создать класс-делегат для отслеживания данных.

Самым первым методом реализуем отслеживание состояния авторизации:

extension ViewController: CLLocationManagerDelegate {
    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        //Статус авторизации
        switch status {
        //Если он не определён (то есть ни одного запроса на авторизацию не было, то попросим базовую авторизацию)
        case .notDetermined:
            manager.requestWhenInUseAuthorization()
        //Если она ограничена или запрещена, то уведомим об отключении
        case .restricted, .denied:
            print("Отключаем локацию")
        //Если авторизация базовая, то попросим предоставить полную
        case .authorizedWhenInUse:
            print("Включаем базовые функции")
            manager.requestAlwaysAuthorization()
        //Хи-хи
        case .authorizedAlways:
            print("Теперь мы знаем, с кем Вы трахаетесь")
        }
    }
}

Делегат использует этот метод, так как разрешение на геопозицию может измениться в любой момент.

Теперь добавим метод, срабатывающий при получении набора позиций (или иначе, локаций)

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    for location in locations {

    }
}

Прежде, чем двинемся дальше подумаем над следующим - нам нужно будет выводить в консоль не просто данные о смене позиции, но и время, когда это случилось. Можно конечно создать обёртку над print, но существует несколько способов вывода таких данных, что называется логированием. Мы будем использовать NSLog:

let altitude = location.altitude
NSLog("Высота над уровнем моря: \(altitude)\n")

Данная величина равна высоте устройства (в метрах) над уровнем моря. Отрицательное значение говорит о том, что устройство находится под уровнем моря.

Позицию любого объекта на Земле можно определить по двумерным координатам - широте и долготе:

let coordinate = location.coordinate
NSLog("Широта \(coordinate.latitude), долгота \(coordinate.longitude)\n")

Широта определяет позицию по горизонтальным осям относительно экватора (линии по середине Земли), а долгота - по вертикали. При таком поиске позиции Земля выступает как центр сферической системы координат, что вполне удобно.

Учтите, что расстояние в градусах между объектами нельзя линейно преобразовать в дистанцию в метрах.

В некоторых случаях геопозиция позволяет получить этаж здания, на котором Вы находитесь:

let floor = location.floor
NSLog("Этаж \(floor?.level)\n")

Такая информация доступна не всегда, поэтому не полагайтесь сильно на этот метод.

Также возможно получить мгновенную скорость устройства:

let speed = location.speed
NSLog("Скорость \(speed)\n")

Данная скорость является мгновенной, а не той, что можно увидеть при движении. То есть она может меняться постоянно между получением локаций. Apple рекомендует использовать её только для информационных целей. Скорость измеряется в направлении движения в метрах на секунду. Отрицательная скорость говорит о неверном значении скорости.

Помимо этого можно получить значение курса - направления в градусах относительно севера:

let course = location.course
NSLog("Азимут \(course)\n")

У любого измерения есть точность - ни один из имеющихся способов не даст точной позиции в пространстве. При этом вертикальная и горизонтальная точность различаются:

let horizontalAccuracy = location.horizontalAccuracy
let verticalAccuracy = location.verticalAccuracy
NSLog("Точность: (\(horizontalAccuracy), \(verticalAccuracy))\n")

Под точностью понимается круг с радиусом, равным точности в горизонтальном или вертикальном направлении, в пределах которого может располагаться объект.

И напоследок можно получить метку времени, когда произошло изменение:

let timestamp = location.timestamp
NSLog("Временная метка \(timestamp)\n")

Обратите внимание, что метод получает позицию не всегда в единственном числе, а целым массивом. Вручную запросить текущее положение можно с помощью метода менеджера позиции requestLocation()

И последним штрихом добавим метод для отслеживания ошибок:

func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
    //Если ошибку можно превратить в ошибку геопоозии, то сделаем это
    guard let locationError = error as? CLError else {
        //Иначе выведем как есть
        print(error)
        return
    }

    //Если получилось, то можно получить локализованное описание ошибки
    NSLog(locationError.localizedDescription)
}

Если не реализовать данный метод и ошибка произойдёт, то сервис геолокации выбросит её в качестве исключения, которое ловить Вам будет собственно нечем.


Отслеживание позиции

Собственно сейчас всё хорошо. Кроме одного. Отслеживать позицию нам просто нечем - мы не создали никаких сервисов для обработки позиции. Исправим это, создав соответствующее свойство:

let manager: CLLocationManager = {
    let locationManager = CLLocationManager()

    locationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers
    //Точность измерения самой системой - чем она лучше, тем больше энергии потребляеет приложение. Задаётся через набор k-констант. Старайтесь использовать ту точность, что реально важна для приложения
    locationManager.distanceFilter = 10
    //Свойство отвечает за фильтр дистанции - величину, лишь при изменении на которую будет срабатывать изменение локации

    locationManager.pausesLocationUpdatesAutomatically = true
    //Позволяет системе автоматически останавливать обновление локации для балансировщика энергии
    locationManager.activityType = .fitness
    //Через это свойство Вы можете указать тип действий, для которого используется геопозиция, это позволит системе лучше обрабатывать балансировку геопозиции
    locationManager.showsBackgroundLocationIndicator = true
    //С помощью этого свойства мы решаем, показывать или нет значок геопозиции для работы в фоновом режиме
    return locationManager
}()

Чтобы отслеживать остановку и возобновление отслеживания, Вы можете подписаться делегатом на методы locationManagerDidPauseLocationUpdates(_:) для остановки и продолжения locationManagerDidResumeLocationUpdates(_:)

И последним штрихом будет добавление двух строчек в конец viewDidLoad:

//Назначение делегата для менеджера
manager.delegate = self
//Начало обновления позиции
manager.startUpdatingLocation()

Для всех методов начала отслеживания (со слова start) есть аналоги со словом stop для остановки.

Запустите приложение и у Вас появится два запроса на использование геопозиции:

Согласитесь и Вы начнете получать сообщения в консоли.

При работе на симуляторе у Вас нет реального источника геопозиции. Но Вы можете задать нужную локацию или режим работы через Debug -> Location -> {Выбор}.

Вы можете:


Расстояние между двумя объектами на Земле

Давайте сделаем отображение расстояния, пройденного человеком между обновлениями позиции.

Вы можете выбрать другую точность измерения позиции, если трёх километров будет слишком много.

Создадим сперва свойство контроллера для отслеживания предыдущей позиции:

var previousLocation: CLLocation?

Теперь на контроллер добавьте метку, которую мы будем использовать для отслеживания позиции:

Теперь немного доработаем наш метод отслеживания получения геопозоции в самом конце:

let lastLocation = locations.last!

Массив с локациями содержит их в порядке получения. Гарантируется, что он будет иметь хотя бы один элемент. Причём последним элементом будет последняя позиция.

//Если уже есть одна позиция, то мы можем рассчитать дистанцию
if let previousLocation = previousLocation {
    pathLabel.text = String(round(lastLocation.distance(from: previousLocation)))
}
//А затем заменить старую позицию на новую
previousLocation = lastLocation

Вычисление расстояния между позициями происходит с учетом того, что Земля - круглая, для этого по поверхности земли строится гладкая кривая, соединяющая точки. Заметьте, что этот метод не учитывает проходимости участка и изменения высот на пути.
Запустите приложение и Вы получите примерно следующее (если выбрать симуляцию локации Freeway Drive):

Учтите, что этот метод сработает только на Земле. Вы не получите значащих данных, если воспользуетесь им на какой-то другой планете.


Определение азимута

Сперва закомментируйте вывод данных о текущей позиции в методе обновления позиций. (Это снизит засорение консоли в процессе работы.)

Ранее мы получали значение курса - курс отображает направление движения относительно севера. Heading или же настоящий азимут отображает направление устройства относительно севера.

Для расчёта курса и скорости необходимы GPS, а для расчёта настоящего азимута - магнетометр. Магнетометр отсутсвует в симуляторе, так что получить результат Вы сможете лишь на реальном устройстве.

Сперва реализуем метод, который попросит пользователя провести калибровку устройства:

func locationManagerShouldDisplayHeadingCalibration(_ manager: CLLocationManager) -> Bool {
    return true
}

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

Также реализуем метод делегата, отвечающий за получение нового направления:

func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
    NSLog("\(newHeading.magneticHeading)")
    //Первое свойство даёт направление в градусах к магнитному полюсу Земли

    NSLog("\(newHeading.trueHeading)")
    //Второе позволяет получить направление к истинному северному полюсу (географический полюс отличается от реального)

    NSLog("\(newHeading.headingAccuracy)")
    //Точность в градусах, показывающая пределы отклонения измерений от реального полюса. Учтите, что отрицательное значение говорит о получении неверной и неприменимой информации

    NSLog("\(newHeading.timestamp)")
    //Метка времени получения данных

    let vector = (x: newHeading.x, y: newHeading.y, z: newHeading.z)
    //Также можно получить чистый вектор направления к полюсу, что полезно в программах навигации для изменения положения карты
    NSLog("\(vector)")
}

По-умолчанию принимается, что верх устройства совпадает с действительным севером. Однако не все приложения работают в этой ориентации, а потому её можно задать с помощью свойства headingOrientation.

Последим штрихом станет добавление запроса на обновление азимута в viewDidLoad:

manager.startUpdatingHeading()

Для получения нормальных результатов стоит оставить и обновление геопозиции, даже если она Вам не нужна.


Явное требование на возможности устройства

Вы можете в явном виде потребовать от устройства иметь те или иные функции через Info.plist, массив Required device capabilities, в который можно вписать следующее:

В отличие от разрешения пользователя, эта часть отвечает за возможность использовать приложение на устройстве, если тот или иной функционал доступен. Если его нет, то App Store просто не даст установить приложение.