SA1023 — Используйте данные о местоположении
Содержание страницы
Вы научились получать информацию о координатах GPS с устройства и отображать ее на экране.
В этой главе вы узнаете следующее:
- Обработка ошибок GPS: Получение информации GPS-это процесс, подверженный ошибкам. Как вы справляетесь с ошибками?
- Улучшение результатов GPS: Как повысить точность результатов GPS, которые вы получаете.
- Обратное геокодирование: получение адреса для заданного набора GPS — координат.
- Тестирование на устройстве: тестирование на устройстве, чтобы убедиться, что ваше приложение обрабатывает реальные сценарии.
- Поддержка различных размеров экрана: настройка пользовательского интерфейса для работы на устройствах iOS с различными размерами экрана.
Обрабатывать ошибки GPS
Получение GPS-координат подвержено ошибкам. Возможно, вы находитесь где-то там, где нет четкой прямой видимости неба-например, внутри или в районе с большим количеством высоких зданий, — блокируя сигнал GPS.
Возможно, вокруг вас не так много Wi-Fi-маршрутизаторов, или они еще не каталогизированы, так что Wi-Fi-радио тоже не очень помогает определить местоположение. И, конечно, ваш сотовый сигнал может быть настолько слабым, что триангуляция вашего местоположения также не даст особенно хороших результатов.
Все это при условии, что на вашем устройстве действительно есть GPS или сотовое радио. Я просто вышел со своим iPod touch, чтобы захватить координаты и получить несколько фотографий для этого приложения. В центре города ему не удалось зафиксировать свое местоположение. Мой iPhone работал лучше, но все равно не был идеальным.
Мораль этой истории заключается в том, что ваши приложения, ориентированные на местоположение, лучше знают, как справляться с ошибками и плохими показаниями. Нет никаких гарантий, что вы сможете установить местоположение, а если и сможете, то это все равно может занять несколько секунд.
Именно здесь программное обеспечение встречается с реальным миром. Вы должны добавить в приложение код обработки ошибок, чтобы пользователи знали о проблемах с получением этих координат.
Код обработки ошибок
- Добавьте эти две переменные экземпляра в CurrentLocationViewController.swift:
var updatingLocation = false
var lastLocationError: Error?
locationManager(_:didFailWithError:)
Изменение на следующее :
func locationManager(
_ manager: CLLocationManager,
didFailWithError error: Error
) {
print("didFailWithError \(error.localizedDescription)")
if (error as NSError).code == CLError.locationUnknown.rawValue {
return
}
lastLocationError = error
stopLocationManager()
updateLabels()
}
Диспетчер местоположений может сообщать об ошибках для различных сценариев. Вы можете посмотреть на code
свойство Error
объекта, чтобы узнать, с каким типом ошибки вы имеете дело. Вам нужно сначала привести NSError
, так как это подкласс Error
, который на самом деле содержит свойство code
.
Некоторые из возможных ошибок в расположении ядра:
CLError.locationUnknown
— местоположение в настоящее время неизвестно, но Основное местоположение будет продолжать пытаться.CLError.denied
— пользователь отказал приложению в разрешении на использование геолокационных служб.CLError.network
— произошла ошибка, связанная с сетью.
Есть и другие, но вы поняли, в чем дело. Куча причин для того, чтобы что-то пошло не так!
Примечание: Эти коды ошибок определены в
CLError
перечислении. Напомним , что перечисление илиenum
— это список значений и имен для этих значений.Коды ошибок, используемые Core Location, имеют простые целочисленные значения. Вместо того чтобы использовать значения 0, 1, 2 и так далее в вашей программе, Core Location дала им символические имена с помощью
CLError
перечисления. Это делает эти коды более понятными, и у вас меньше шансов выбрать неправильный.Чтобы преобразовать эти имена обратно в целочисленное значение, вы запрашиваете
rawValue
.
В своей обновленной locationManager(_:didFailWithError:)
версии вы делаете:
if (error as NSError).code == CLError.locationUnknown.rawValue {
return
}
Ошибка CLError.locationUnknown
означает, что менеджер местоположений не смог получить местоположение прямо сейчас, но это не значит, что все потеряно. Возможно, потребуется еще секунда или около того, чтобы получить восходящую связь со спутником GPS. В то же время он дает вам знать, что на данный момент он не может получить никакой информации о местоположении.
Когда вы получите эту ошибку, вы просто будете продолжать попытки, пока не найдете местоположение или не получите более серьезную ошибку.
В случае более серьезной ошибки вы сохраняете объект error в новой переменной экземпляра, lastLocationError
:
lastLocationError = error
Таким образом, позже вы сможете посмотреть, с какой ошибкой вы имели дело. Это пригодится в updateLabels()
будущем . Вскоре вы измените этот метод, чтобы показать ошибку пользователю, потому что не хотите оставлять его в неведении о таких вещах.
Упражнение: Можете ли вы объяснить, почему
lastLocationError
это необязательно?
Ответ: Когда ошибки нет, lastLocationError
значения иметь не будет. Другими словами, это может быть nil
, и переменные, которые могут бытьnil
, должны быть опционными в Swift.
Наконец, обновление locationManager(_:didFailWithError:)
добавляет новый вызов метода:
stopLocationManager()
Остановка обновления местоположения
Если получение местоположения кажется невозможным для того места, где пользователь в данный момент находится на земном шаре, то вам нужно сказать менеджеру местоположений, чтобы он остановился. Чтобы сэкономить заряд батареи, приложение должно выключить радиоприемники iPhone, как только они ему больше не понадобятся.
Если бы это было пошаговое навигационное приложение, вы бы продолжали работать с диспетчером местоположений даже в случае сетевой ошибки, потому что, кто знает, на пару метров вперед вы могли бы получить действительное местоположение.
Для этого приложения пользователю просто нужно будет снова нажать кнопку «Получить мое местоположение», если он захочет попробовать в другом месте.
- Добавьте
stopLocationManager()
метод:
func stopLocationManager() { if updatingLocation { locationManager.stopUpdatingLocation() locationManager.delegate = nil updatingLocation = false } }
Существует if
оператор, который проверяет, является ли переменная экземпляра boolean переменной updatingLocation``true
or false
. Если это так false
, то диспетчер местоположений в данный момент не активен, и нет необходимости останавливать его.
Причина наличия этой переменной updatingLocation
заключается в том, что вы собираетесь изменить внешний вид кнопки Get My Location и метки сообщения о состоянии, когда приложение пытается получить исправление местоположения, чтобы пользователь знал, что приложение работает над ним.
- Введите дополнительный код,
updateLabels()
чтобы показать сообщение об ошибке.:
func updateLabels() { if let location = location { . . . } else { . . . // Remove the following line messageLabel.text = "Tap 'Get My Location' to Start" // The new code starts here: let statusMessage: String if let error = lastLocationError as NSError? { if error.domain == kCLErrorDomain && error.code == CLError.denied.rawValue { statusMessage = "Location Services Disabled" } else { statusMessage = "Error Getting Location" } } else if !CLLocationManager.locationServicesEnabled() { statusMessage = "Location Services Disabled" } else if updatingLocation { statusMessage = "Searching..." } else { statusMessage = "Tap 'Get My Location' to Start" } messageLabel.text = statusMessage } }
Новый код определяет, что нужно поместить messageLabel
в верхнюю часть экрана. Он использует кучу if
утверждений, чтобы выяснить, каков текущий статус приложения.
Если диспетчер местоположений выдал ошибку, на этикетке появится сообщение об ошибке.
Первая ошибка , которую он проверяет, находится CLError.denied
в домене ошибокkCLErrorDomain
, что означает ошибки расположения ядра. В этом случае пользователь не дал этому приложению разрешения на использование геолокационных служб. Это вроде как противоречит цели этого приложения, но это может случиться, и вы все равно должны проверить это. Если код ошибки-это что-то другое, то вы просто говорите “Ошибка получения местоположения”, так как это обычно означает, что не было никакого способа получить исправление местоположения.
Даже если ошибки не было, получить координаты местоположения все равно было бы невозможно, если бы пользователь полностью отключил службы определения местоположения на своем устройстве, а не только для этого приложения. Вы проверяете эту ситуацию с locationServicesEnabled()
помощью метода CLLocationManager
.
Предположим, ошибок не было и все работает нормально, тогда метка состояния скажет “Поиск…” до того, как будет получен первый объект местоположения.
Если ваше устройство может быстро получить исправление местоположения, то этот текст будет виден только в течение доли секунды, но часто для получения этого первого исправления местоположения может потребоваться некоторое время. Никто не любит ждать, поэтому приятно сообщить пользователю, что приложение активно ищет его местоположение. Это то, для чего вы используете updatingLocation
логическое значение.
Примечание: Вы помещаете всю эту логику в один метод, потому что это позволяет легко изменить экран, когда что-то изменилось. Получил место? Просто позвоните
updateLabels()
, чтобы обновить содержимое экрана. Получили ошибку? ДавайтеupdateLabels()
разберемся…
Начать обновление местоположения
- Также добавьте новый
startLocationManager()
метод — я предлагаю вам поместить его прямо вышеstopLocationManager()
, чтобы сохранить связанные функции вместе.:
func startLocationManager() { if CLLocationManager.locationServicesEnabled() { locationManager.delegate = self locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters locationManager.startUpdatingLocation() updatingLocation = true } }
Запуск диспетчера местоположений раньше происходил в методе getLocation()
action. Однако, поскольку теперь у stopLocationManager()
вас есть метод, имеет смысл переместить начальный код в собственный метод, просто чтобы все было симметрично.
Единственное отличие от предыдущего заключается в том, что при этом проверяется, включены ли службы определения местоположения, и вы устанавливаете переменную updatingLocation
в true
значение, действительно ли вы запустили обновления местоположения.
Изменение на getLocation()
:
@IBAction func getLocation() { . . . if authStatus == .denied || authStatus == .restricted { . . . } // New code below, replacing existing code after this point startLocationManager() updateLabels() }
Есть еще одно крошечное изменение. Предположим, произошла ошибка, и никакое местоположение не может быть получено, но затем вы немного походите вокруг, и появляется действительное местоположение. В этом случае неплохо бы удалить старый код ошибки.
- В нижней части
locationManager(_:didUpdateLocations:)
добавьте следующую строку непосредственно перед вызовомupdateLabels()
:
lastLocationError = nil
Это очищает старое состояние ошибки. После получения действительной координаты любая предыдущая ошибка, с которой вы могли столкнуться, больше не применима.
- Запустите приложение и нажмите Получить мое местоположение. Пока приложение ожидает входящих координат, надпись вверху должна гласить “Поиск…” до тех пор, пока оно не найдет правильную координату или не обнаружит фатальную ошибку.

Приложение ждет получения GPS — координат
Поиграйте некоторое время с настройками местоположения симулятора и посмотрите, что происходит, когда вы выбираете разные локации.
Обратите внимание, что изменение местоположения симулятора на Нет больше не является ошибкой. Это все еще возвращает код .locationUnknown
ошибки, но вы игнорируете его, потому что это не фатальная ошибка.
В идеале вы должны тестировать не только в симуляторе, но и на своем устройстве, так как таким образом вы с большей вероятностью столкнетесь с реальными ошибками.
Улучшение результатов GPS
Круто, вы знаете, как получить CLLocation
объект из основного местоположения, и вы можете обрабатывать ошибки. И что теперь?
Ну, вот в чем дело: вы видели в симуляторе, что Core Location продолжает давать вам новые объекты местоположения снова и снова, даже если координаты, возможно, не изменились. Это потому, что пользователь может быть в движении, и в этом случае его GPS-координаты действительно меняются.
Однако вы не создаете навигационное приложение. Таким образом, для MyLocations вы просто хотите получить достаточно точное местоположение, а затем вы можете сказать менеджеру местоположений прекратить отправку обновлений.
Это важно, потому что получение обновлений местоположения требует большого заряда батареи, так как для этого устройство должно поддерживать питание своих GPS /Wi-Fi/ сотовых радиостанций. Этому приложению не нужно постоянно запрашивать GPS-координаты, поэтому оно должно остановиться, когда местоположение будет достаточно точным.
Проблема в том, что вы не всегда можете получить нужную точность, поэтому вам нужно это обнаружить. Когда последние пару координат, которые вы получили, не увеличиваются в точности, это, вероятно, так же хорошо, как и должно быть, и вы должны отключить питание радио.
Получение результатов для определенного уровня точности
Изменение на следующее locationManager(_:didUpdateLocations:)
: :
func locationManager(
_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]
) {
let newLocation = locations.last!
print("didUpdateLocations \(newLocation)")
// 1
if newLocation.timestamp.timeIntervalSinceNow < -5 {
return
}
// 2
if newLocation.horizontalAccuracy < 0 {
return
}
// 3
if location == nil || location!.horizontalAccuracy > newLocation.horizontalAccuracy {
// 4
lastLocationError = nil
location = newLocation
// 5
if newLocation.horizontalAccuracy <= locationManager.desiredAccuracy {
print("*** We're done!")
stopLocationManager()
}
updateLabels()
}
}
Давайте рассмотрим эти изменения один за другим:
- Если время, в которое был определен данный объект местоположения, слишком велико — в данном случае 5 секунд, — то это кэшированный результат. Вместо того чтобы возвращать новое исправление местоположения, менеджер местоположений может сначала дать вам самое последнее найденное местоположение, предполагая, что вы, возможно, не сильно переместились за последние несколько секунд — очевидно, это не учитывает людей с реактивными ранцами. Вы просто проигнорируете эти кэшированные местоположения, если они слишком старые.
- Чтобы определить, являются ли новые показания более точными, чем предыдущие, вы будете использовать
horizontalAccuracy
свойство объекта location. Однако иногда местоположения могут иметь значение ahorizontalAccuracy
меньше 0. В этом случае эти измерения недействительны, и вы должны игнорировать их. - Именно здесь вы определяете, является ли новое чтение более полезным, чем предыдущее. Вообще говоря, Core Location начинается с довольно неточных показаний, а затем с течением времени дает вам все более и более точные данные. Однако никаких гарантий нет — поэтому вы не можете предполагать, что следующее чтение действительно всегда будет более точным. Обратите внимание, что большее значение точности означает меньшую точность — в конце концов, точность до 100 метров хуже, чем точность до 10 метров. Вот почему вы проверяете, больше ли предыдущее чтение ,
location!.horizontalAccuracy
чем новое чтение,newLocation.horizontalAccuracy
.
Вы также проверяетеlocation == nil
.
Напомним, чтоlocation
это необязательная переменная экземпляра, в которой хранитсяCLLocation
объект, полученный в предыдущем вызовеdidUpdateLocations
. Еслиlocation
—nil
, то это самое первое обновление местоположения, которое вы получаете, и в этом случае вы должны продолжить.
Итак, если это самое первое считывание местоположения (location
isnil
) или новое местоположение более точное, чем предыдущее, вы переходите к шагу 4. В противном случае вы игнорируете это обновление местоположения. - Вы уже видели эту часть раньше. Он очищает любую предыдущую ошибку и сохраняет новый
CLLocation
объект вlocation
переменной. - Если точность нового местоположения равна или превышает желаемую точность, вы можете закрыть его на сегодня и перестать запрашивать обновления у менеджера местоположения. Когда вы запустили диспетчер местоположений
startLocationManager()
, вы установили желаемую точность на 10 метров (kCLLocationAccuracyNearestTenMeters
), что достаточно хорошо для этого приложения.
Короткое замыкание
Поскольку location
— это необязательный объект, вы не можете получить доступ к его свойствам напрямую — сначала вам нужно его развернуть. Вы можете сделать это с if let
помощью , но если вы уверены, что это не nil
так, вы также можете принудительно развернуть его !
.
Вот что вы делаете в этой линии:
if location == nil || location!.horizontalAccuracy > newLocation.horizontalAccuracy {
Вы написали location!.horizontalAccuracy
с восклицательным знаком, а не просто location.horizontalAccuracy
.
Но что , если location == nil
тогда не удастся развернуть силу? Не в этом случае, потому что силовое разворачивание никогда не выполняется.
Оператор ||
(логическое или) проверяет, истинно ли одно из этих двух условий. Если первое условие истинно (location
is nil
), оно не будет оценивать второе условие. Это называется коротким замыканием. Приложению нет необходимости проверять второе условие, если первое уже истинно.
Таким образом, приложение будет смотреть только тогдаlocation!.horizontalAccuracy
, когда location
гарантированно не будет nil
. Сносит тебе крышу, а?
- Запустите приложение. Сначала установите местоположение симулятора равным None, затем нажмите Get My Location. На экране появилась надпись “Поиск…”.
- Переключите местоположение на Apple, но больше не нажимайте кнопку «Получить мое местоположение». Через некоторое время экран обновляется с помощью GPS-координат по мере их поступления.
Если вы проверите консоль Xcode, вы получите около 10 обновлений местоположения, прежде чем он скажет: “ * * * Мы закончили!” и обновления местоположения прекратятся.
Примечание: Вполне возможно, что описанные выше шаги не сработают для вас. Если на экране не написано “Поиск…”, а вместо этого показан старый набор координат, то Симулятор сохраняет старые данные о местоположении. Кажется, это происходит, когда вы выбираете местоположение из Xcode, используя стрелку в области отладки вместо меню функций симулятора.
Самый быстрый способ исправить это — выйти из симулятора и запустить приложение снова — это запускает новый симулятор. Если вы не можете заставить его работать, не беспокойтесь, это не так уж важно. Просто имейте в виду, что Симулятор иногда может быть привередливым.
Вы, как разработчик, можете определить с консоли, когда обновление местоположения прекратится, но очевидно, что пользователь этого не увидит.
Кнопка «Местоположение тега» становится видимой, как только получено первое местоположение, так что пользователь может сразу же начать сохранять это местоположение в своей библиотеке, но на данный момент местоположение может быть недостаточно точным.
Поэтому приятно показать пользователю, когда приложение нашло наиболее точное местоположение.
Обновите пользовательский интерфейс
Чтобы сделать это более ясным, вы собираетесь переключить кнопку “Получить мое местоположение”, чтобы сказать “Стоп”, когда захват местоположения активен, и переключить ее обратно на «Получить мое местоположение», когда это будет сделано. Это дает пользователю хорошую визуальную подсказку. Позже вы также покажете анимированный счетчик активности, который сделает это еще более очевидным.
Чтобы изменить состояние кнопки, вы добавите configureGetButton()
метод.
- Добавьте следующий метод в CurrentLocationViewController.swift:
func configureGetButton() { if updatingLocation { getButton.setTitle("Stop", for: .normal) } else { getButton.setTitle("Get My Location", for: .normal) } }
Все очень просто: если приложение в данный момент обновляет местоположение, то заголовок кнопки становится Stop, в противном случае — Get My Location.
Теперь вам нужно позвонить configureGetButton()
из нескольких разных мест в вашем коде. Если вы присмотритесь, то заметите , что везде, где вы вызываете updateLabels()
, вам также нужно вызвать новый метод. Так что с таким же успехом можно вызвать новый метод изнутри updateLabels()
, не так ли?
- Добавить вызов
configureGetButton()
в концеupdateLabels()
:
func updateLabels() {
. . .
configureGetButton()
}
- Запустите приложение еще раз и выполните тот же тест, что и раньше. Кнопка меняется на Стоп, когда вы ее нажимаете. Когда больше нет обновлений местоположения, он переключается обратно.
Когда кнопка говорит “Стоп”, вы, естественно, ожидаете, что сможете нажать ее, чтобы прервать обновление местоположения. Это особенно важно, когда вы вообще не получаете никаких координат. В конце концов Core Location может выдать ошибку, но как пользователь вы, возможно, не захотите этого ждать.
Однако в настоящее время нажатие кнопки Stop ничего не останавливает. Вы должны измениться getLocation()
для этого, так как любое нажатие на кнопку вызывает этот метод.
- Войдя в
getLocation()
систему , замените строку вызовом наstartLocationManager()
следующую:
if updatingLocation { stopLocationManager() } else { location = nil lastLocationError = nil startLocationManager() }
Опять же, вы используете updatingLocation
флаг, чтобы определить, в каком состоянии находится приложение.
Если кнопка нажата, когда приложение уже выполняет выборку местоположения, вы останавливаете диспетчер местоположений.
Обратите внимание, что вы также очищаете старое местоположение и объекты ошибок, прежде чем начать поиск нового местоположения.
BOS Запустите приложение. Теперь нажатие кнопки «Стоп» положит конец обновлениям местоположения. После нажатия кнопки Stop вы больше не увидите обновлений в консоли.
Примечание: Если кнопка «Стоп» появляется недостаточно долго, чтобы вы могли нажать ее, сначала установите значение «Нет», нажмите «Получить мое местоположение» несколько раз, а затем снова выберите местоположение Apple.
Обратное геокодирование
GPS-координаты, с которыми вы до сих пор имели дело, — это просто цифры. Координаты 37.332440904, -122.03051218 на самом деле не так уж много значат, но адрес 1 Infinite Loop в Купертино, штат Калифорния, действительно значит.
Используя процесс, известный как обратное геокодирование, вы можете превратить набор координат в удобочитаемый адрес. Регулярное или “прямое” геокодирование делает обратное: оно превращает адрес в координаты GPS. Вы можете сделать и то, и другое с помощью iOS SDK, но для MyLocations вы будете делать только обратное.
Вы будете использовать CLGeocoder
объект, чтобы превратить данные о местоположении в удобочитаемый адрес, а затем отобразить этот адрес на экране.
Сделать это довольно легко, но есть некоторые правила. Вы не должны посылать тонну этих запросов на обратное геокодирование одновременно. Процесс обратного геокодирования происходит на сервере, размещенном компанией Apple, и обработка этих запросов обходится им в пропускную способность и процессорное время. Если вы завалите их серверы запросами, Apple не обрадуется.
Предполагается, что MyLocations используется только изредка. Таким образом, теоретически его пользователи не будут рассылать спам по серверам Apple, но вы все равно должны ограничить запросы геокодирования одним запросом за раз и по одному разу для каждого уникального местоположения. В конце концов, нет смысла повторно геокодировать один и тот же набор координат снова и снова.
Обратное геокодирование требует активного подключения к Интернету, и все, что вы можете сделать, чтобы предотвратить ненужное использование радиостанций iPhone, хорошо для ваших пользователей.
Осуществление
- Добавьте следующие свойства в CurrentLocationViewController.swift:
let geocoder = CLGeocoder() var placemark: CLPlacemark? var performingReverseGeocoding = false var lastGeocodingError: Error?
Они отражают то, что вы сделали для менеджера местоположения. CLGeocoder
это объект, который будет выполнять геокодирование, и CLPlacemark
— это объект, который содержит результаты адреса.
Переменная placemark
должна быть необязательной, потому что она не будет иметь никакого значения, когда еще нет местоположения, или когда местоположение не соответствует уличному адресу — я не думаю, что он ответит “Пустыня Сахара, Африка”, но, честно говоря, у меня не было возможности попробовать.
Вы устанавливаете performingReverseGeocoding``true
значение, когда выполняется операция геокодирования, и lastGeocodingError
будете содержать Error
объект, если что-то пошло не так или nil
если ошибки нет.
Вы поставите геокодер работать в locationManager(didUpdateLocations)
:
`func locationManager(
_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]
) {
. . .
if location == nil || location!.horizontalAccuracy > newLocation.horizontalAccuracy {
. . .
if newLocation.horizontalAccuracy <= locationManager.desiredAccuracy {
. . .
}
updateLabels()
// The new code begins here:
if !performingReverseGeocoding {
print(«*** Going to geocode»)
performingReverseGeocoding = true
geocoder.reverseGeocodeLocation(newLocation) {placemarks, error in
if let error = error {
print("*** Reverse Geocoding error: \(error.localizedDescription)")
return
}
if let places = placemarks {
print("*** Found places: \(places)")
}
}
}
// End of the new code
}
}`
Приложение должно выполнять только один запрос обратного геокодирования за один раз. Итак, сначала вы проверяете, занят ли он, глядя на performingReverseGeocoding
переменную. Затем вы запускаете геокодер.
Код выглядит достаточно простым, не так ли? Обратите внимание на закрытие в конце вызова reverseGeocodeLocation
— это трейлинг-закрытие, похожее на то, что вы видели раньше несколько раз.
Закрытия
В отличие от диспетчера местоположений, CLGeocoder
он не использует делегат для возврата результатов операции. Вместо этого он использует замыкание. Закрытие — важная функция Swift, и вы можете ожидать увидеть их повсюду-для программистов Objective-C закрытие похоже на “блок”.
Замыкания также могут иметь параметры, и здесь параметрами для замыкания являются placemarks
and error
, оба из которых являются опционными, потому что либо одно, либо другое может быть nil
в зависимости от ситуации.
Таким образом, хотя весь код внутри замыкания выводит либо список мест, либо ошибку, вам нужно развернуть каждый необязательный параметр, прежде чем сделать это, чтобы убедиться, что у вас есть значение.
В отличие от остальной части кода locationManager(_:didUpdateLocations:)
, код в закрытии выполняется не сразу. В конце концов, вы можете распечатать результаты геокодирования только после его завершения, а это может произойти через несколько секунд.
Закрытие сохраняется для последующего использования CLGeocoder
объектом и выполняется только после CLGeocoder
того, как найден адрес или обнаружена ошибка.
Так почему же CLGeocoder
используется закрытие вместо делегата?
Проблема с использованием делегата для предоставления обратной связи заключается в том, что вам нужно написать один или несколько отдельных методов. Например, для CLLocationManager
существуют методы locationManager(_:didUpdateLocations:)
и.locationManager(_:didFailWithError:)
Создавая отдельные методы, вы удаляете код, который обрабатывает ответ, от кода, который делает запрос. С другой стороны, с помощью замыканий вы можете поместить этот код обработки в одно и то же место. Это делает код более компактным и легким для чтения. Некоторые API делают и то, и другое, и у вас есть выбор между использованием закрытия или становлением делегатом.
Поэтому, когда вы пишете,
geocoder.reverseGeocodeLocation(newLocation) {placemarks, error in // put your statements here }
вы говорите CLGeocoder
объекту, что хотите выполнить обратное геокодирование местоположения и что код в закрытии должен быть выполнен, как только геокодирование будет завершено.
Само закрытие-это:
{ placemarks, error in // put your statements here }
Элементы перед in
ключевым словом — placemarks
и error
— являются параметрами для этого замыкания и работают точно так же, как параметры для метода или функции.
Когда геокодер находит результат для объекта местоположения, который вы ему дали, он вызывает закрытие и выполняет инструкции внутри. placemarks
Параметр будет содержать массив CLPlacemark
объектов, описывающих адресную информацию, а error
переменная-сообщение об ошибке на случай, если что-то пойдет не так.
Замыкания-это в основном тот же принцип, что и использование методов делегата, за исключением того, что вы помещаете код не в отдельный метод, а в замыкание.
Ничего страшного, если закрытие заставило вас прямо сейчас почесать голову. Вы увидите, как они будут использоваться еще много раз в следующих главах.
BOS Запустите приложение и выберите место. Как только первое местоположение найдено, вы можете увидеть в консоли, что обратный геокодер заработал через секунду или две:
didUpdateLocations <+37.33233141,-122.03121860> +/- 5.00m (speed 0.00 mps / course -1.00) @ 7/24/21, 8:05:35 AM Eastern Daylight Time *** Going to geocode *** Found places: [Apple Campus, Apple Campus, 1 Infinite Loop, Cupertino, CA 95014, United States @ <+37.33233141,-122.03121860> +/- 100.00m, region CLCircularRegion (identifier:'<+37.33213110,-122.02990105> radius 279.38', center:<+37.33213110,-122.02990105>, radius:279.38m)]
Если вы выберете местоположение Apple, то увидите, что некоторые показания местоположения дублируются; геокодер выполняет только первое из них. Только когда точность считывания улучшается, приложение снова меняет геокодирование. Мило!
Примечание: Несколько читателей ранее сообщали, что если вы находитесь в Китае и пытаетесь изменить геокодирование адреса, который находится за пределами Китая, вы можете получить ошибку и
placemarks
будетеnil
ею . Если это случится с вами, попробуйте вместо этого найти место в Китае.
Обработка ошибок обратного геокодирования
BOS Замените содержимое замыкания геокодирования следующим:
`self.lastGeocodingError = error
if error == nil, let places = placemarks, !places.isEmpty {
self.placemark = places.last!
} else {
self.placemark = nil
}
self.performingReverseGeocoding = false
self.updateLabels()`
Как и в случае с диспетчером местоположений, вы сохраняете объект error, чтобы ссылаться на него позже — на этот раз вы используете другую переменную экземпляра lastGeocodingError
.
Следующая строка делает то, чего вы раньше не видели:
if error == nil, let places = placemarks, !places.isEmpty {
Вы знаете, что if let
это используется для разворачивания опций. Здесь placemarks
это необязательно, поэтому его нужно развернуть, прежде чем вы сможете его использовать, иначе вы рискуете разбить приложение, когда placemarks
оно есть nil
. Развернутый placemarks
массив получает временное имя places
.
!places.isEmpty
Бит говорит, что мы должны вводить этот if
оператор только в том случае, если массив объектов placemark не пуст.
Вы должны прочитать эту строку так::
if there’s no error and the unwrapped placemarks array is not empty {
Конечно, Свифт не говорит по-английски, поэтому вы должны выразить это в терминах, которые Свифт понимает.
Вы также могли бы написать это как три разных вложенных if
оператора:
if error == nil { if let places = placemarks { if !places.isEmpty {
Но так же легко объединить все эти условия в одно if
целое .
Здесь вы немного занимаетесь защитным программированием: сначала вы специально проверяете, есть ли в массиве какие-либо объекты. Если ошибки нет, то у него должен быть хотя бы один объект, но вы не будете верить, что он всегда будет. Хорошие разработчики-параноики!
Если все три условия выполнены — ошибки нет, placemarks
массива нет nil
, и внутри этого массива есть хотя бы один CLPlacemark
— то вы берете последний из этих CLPlacemark
объектов:
self.placemark = places.last!
last
Свойство ссылается на последний элемент массива. Это необязательно, потому что нет последнего элемента, если массив пуст. В качестве альтернативы вы также можете писатьplaces[places.count - 1]
, но это не так аккуратно.
Обычно в массиве будет только один CLPlacemark
объект, но существует странная ситуация, когда одна координата местоположения может ссылаться на несколько адресов. Это приложение может обрабатывать только один адрес за раз. Итак, вы просто выберете последний вариант, который обычно является единственным.
Если во время геокодирования произошла ошибка, вы устанавливаете self.placemark
значение nil
.
Обратите внимание, что вы не сделали этого для местоположений. Если там была ошибка, вы сохранили предыдущий объект location, потому что он действительно может быть правильным или достаточно хорошим, и это лучше, чем ничего.
Но для адреса это имеет меньше смысла. Вы не хотите показывать старый адрес, только адрес, соответствующий текущему местоположению, или вообще никакого адреса.
В мобильной разработке ничто не гарантировано. Вы можете получить координаты обратно, а можете и не получить, а если и получите, то они могут быть не очень точными. Обратное геокодирование, вероятно, будет успешным, если есть какой-то тип доступного сетевого подключения, но вы также должны быть готовы справиться со случаем, когда его нет.
И помните, что не все GPS — координаты соответствуют реальным уличным адресам-в пустыне Сахара нет угла 52-й улицы и Бродвея.
Примечание: Заметили ли вы, что внутри замыкания вы
self
ссылались на свойства и методы контроллера представления? Это быстрое требование.Говорят, что замыкания захватывают все переменные, которые они используют, и
self
являются одним из них. Вы можете забыть об этом немедленно, если хотите; просто знайте, что Swift требует, чтобы все захваченные переменные были явно упомянуты.Как вы уже видели, вне замыкания вы можете
self
ссылаться на свойства и методы, но это не обязательно. Тем не менее, вы получите ошибку компилятора, если вы оставитеself
внутри закрытия. Так что у вас нет особого выбора.
Отображение адреса
Давайте покажем адрес пользователю.
Изменение наupdateLabels()
:
func updateLabels() { if let location = location { . . . // Add this block if let placemark = placemark { addressLabel.text = string(from: placemark) } else if performingReverseGeocoding { addressLabel.text = "Searching for Address..." } else if lastGeocodingError != nil { addressLabel.text = "Error Finding Address" } else { addressLabel.text = "No Address Found" } // End new code } else { . . . } }
Поскольку вы выполняете поиск адреса только после того, как приложение имеет действительное местоположение, вам просто нужно изменить код внутри первой if
ветви. Если вы нашли адрес, вы показываете его пользователю, в противном случае вы показываете сообщение о состоянии.
Код для форматирования CLPlacemark
объекта в строку помещается в его собственный метод, просто чтобы код был читабельным.
BOS Добавьте string(from:)
метод:
func string(from placemark: CLPlacemark) -> String { // 1 var line1 = "" // 2 if let tmp = placemark.subThoroughfare { line1 += tmp + " " } // 3 if let tmp = placemark.thoroughfare { line1 += tmp } // 4 var line2 = "" if let tmp = placemark.locality { line2 += tmp + " " } if let tmp = placemark.administrativeArea { line2 += tmp + " " } if let tmp = placemark.postalCode { line2 += tmp } // 5 return line1 + "\n" + line2 }
Давайте рассмотрим это подробнее:
- Адрес будет состоять из двух строк текста — создайте новую строковую переменную для первой строки текста.
- Если метка имеет a
subThoroughfare
, добавьте ее в строку. Это необязательное свойство, поэтому выif let
сначала разворачиваете его. Просто чтобы вы знали,subThoroughfare
это причудливое название для номера дома. - Добавление названия
thoroughfare
улицы или названия улицы делается аналогично. Обратите внимание, что вы кладете пространство между ними,subThoroughfare
чтобы они не склеивались вместе. - Та же логика применима и ко второй строке текста. При этом добавляется населенный пункт (город), административный район (штат или провинция) и почтовый индекс (или почтовый индекс) с пробелами между ними, где это уместно.
- Наконец, две строки объединяются или складываются вместе с символом новой строки между ними.
\n
Добавляет разрыв строки (или новую строку) в строку.
Войдите getLocation()
, очистите переменные placemark
andlastGeocodingError
, чтобы начать с чистого листа. Поставьте это прямо над призывом кstartLocationManager()
:
placemark = nil lastGeocodingError = nil
BOS Запустите приложение еще раз. Через несколько секунд после того, как местоположение найдено, метка адреса также должна быть заполнена.

Обратное геокодирование находит адрес для GPS — координат
Довольно часто в адресе отсутствуют номера улиц или другие детали. CLPlacemark
Объект может содержать неполную информацию, поэтому все его свойства являются необязательными. Геокодирование-это не точная наука!
Упражнение: Если вы выберете локации City Bicycle Ride или City Run из меню функций симулятора, вы увидите в консоли, что приложение прыгает через целую кучу разных координат — оно имитирует кого-то, перемещающегося из одного места в другое. Однако координаты на экране и адресная метка меняются не так часто. Почему это так?
Ответ: Логика майлокаций была разработана для того, чтобы найти наиболее точный набор координат для стационарного положения. Вы обновляете location
переменную только тогда, когда появляется новый набор координат, более точный, чем предыдущие показания. Любые новые показания с более высоким или тем же horizontalAccuracy
значением просто игнорируются, независимо от того, каковы фактические координаты.
С опциями City Bicycle Ride и City Run приложение получает не одни и те же координаты с возрастающей точностью, а серию совершенно разных координат. Это означает, что это приложение не очень хорошо работает, когда вы находитесь в движении, если только вы не нажмете Стоп и не попробуете еще раз. С другой стороны, приложение также не было предназначено для захвата меняющихся местоположений.
Примечание: Если вы играете с разными локациями в симуляторе или из меню отладчика Xcode и застряли, то самый быстрый способ отклеиться — сбросить симулятор. Иногда он просто не хочет переезжать на новое место, даже если вы ему скажете, а потом вы должны показать ему, кто здесь хозяин!
Тестирование на устройстве
Когда я впервые написал этот код, я тестировал его только на симуляторе. Там все работало нормально. Затем я поставил его на свой iPod touch, и знаете что? Не очень хорошо.
Проблема с iPod touch заключается в том, что у него нет GPS, поэтому для определения местоположения он полагается только на Wi-Fi. Но Wi-Fi, возможно, не сможет дать вам точность до десяти метров; в лучшем случае я получу +/- 100 метров.
Прямо сейчас вы останавливаете обновление местоположения только тогда, когда точность считывания попадает в пределах desiredAccuracy
заданной настройки — чего на самом деле никогда не произойдет на моем iPod touch.
Это говорит о том, что вы не всегда можете полагаться на симулятор для тестирования своих приложений. Вам нужно поместить их на свое устройство и протестировать в дикой природе, особенно при использовании зависящих от устройства функций, таких как API-интерфейсы на основе местоположения. Если у вас более одного устройства, то протестируйте их все!
Чтобы справиться с этой ситуацией, вы усовершенствуете метод didUpdateLocations
делегата.
Первое исправление
Изменение наlocationManager(_:didUpdateLocations:)
:
`func locationManager(
_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]
) {
. . .
if newLocation.horizontalAccuracy < 0 {
return
}
// New section #1
var distance = CLLocationDistance(Double.greatestFiniteMagnitude)
if let location = location {
distance = newLocation.distance(from: location)
}
// End of new section #1
if location == nil || location!.horizontalAccuracy > newLocation.horizontalAccuracy {
. . .
if newLocation.horizontalAccuracy <= locationManager.desiredAccuracy { . . . // New section #2 if distance > 0 {
performingReverseGeocoding = false
}
// End of new section #2
}
if !performingReverseGeocoding {
. . .
}
updateLabels()
// New section #3
} else if distance < 1 { let timeInterval = newLocation.timestamp.timeIntervalSince(location!.timestamp) if timeInterval > 10 {
print(«*** Force done!»)
stopLocationManager()
updateLabels()
}
// End of new sectiton #3
}
}`
Сейчас это довольно длинный метод, но были добавлены только три выделенных раздела. Это первый из них:
var distance = CLLocationDistance(Double.greatestFiniteMagnitude) if let location = location { distance = newLocation.distance(from: location) }
Это вычисляет расстояние между новым чтением и предыдущим чтением, если оно было. Мы можем использовать это distance
для измерения того, улучшаются ли наши обновления местоположения.
Если предыдущего чтения не было, то расстояние есть Double.greatestFiniteMagnitude
. Это встроенная константа, представляющая максимальное значение, которое Double
может иметь значение. Этот маленький трюк дает ему гигантское расстояние, если это самое первое чтение. Вы делаете это так, чтобы любое из следующих вычислений все еще работало, даже если вы еще не смогли вычислить истинное расстояние.
Вы также добавляете if
оператор позже, когда останавливаете диспетчер местоположений:
if distance > 0 { performingReverseGeocoding = false }
Это приводит к обратному геокодированию конечного местоположения, даже если приложение уже выполняет другой запрос геокодирования.
Вам абсолютно нужен адрес для этого конечного местоположения, так как это самое точное местоположение, которое вы нашли. Но если какое-то предыдущее местоположение все еще подвергалось обратному геокодированию, этот шаг обычно пропускался. Просто установив значение performingReverseGeocoding
to false
, вы всегда заставляете геокодировать эту конечную координату.
Конечно, если distance
равно 0, то это местоположение совпадает с местоположением из предыдущего чтения, и вам больше не нужно его реверсировать геокодированием.
Реальное улучшение можно найти в последнем новом разделе:
} else if distance < 1 { let timeInterval = newLocation.timestamp.timeIntervalSince(location!.timestamp) if timeInterval > 10 { print("*** Force done!") stopLocationManager() updateLabels() } }
Если координата этого считывания существенно не отличается от предыдущего считывания и прошло более 10 секунд с тех пор, как вы получили это исходное считывание, то самое время повесить шляпу и остановиться.
Можно с уверенностью предположить, что вы не получите лучшей координаты, чем эта, и можете перестать извлекать местоположение.
Это улучшение было необходимо для того, чтобы через некоторое время мой iPod touch перестал сканировать. Он не давал мне местоположения с большей точностью, чем +/- 100 метров, но повторял одно и то же снова и снова.
Я выбрал ограничение по времени в 10 секунд, потому что это, казалось, давало хорошие результаты.
Обратите внимание, что вы не просто говорите:
} else if distance == 0 {
Расстояние между последующими показаниями никогда не бывает точно равно 0. Это может быть что-то вроде 0.0017632. Вместо того чтобы проверять, равно ли 0, лучше проверить, меньше ли определенное расстояние, в данном случае один метр.
Кстати, вы заметили, как вы location!
разворачивали его, прежде чем получить доступ к свойству timestamp? Когда вы находитесь внутри этого else-if
, значение location
гарантированно неnil
будет, поэтому безопасно принудительно развернуть необязательный параметр.
BOS Запустите приложение и проверьте, что все по-прежнему работает. Возможно, будет трудно воссоздать эту ситуацию на симуляторе, но попробуйте сделать это на своем устройстве внутри дома и посмотрите, какой вывод вы видите в консоли.
Второе исправление
Есть еще одно улучшение, которое вы можете сделать, чтобы повысить надежность этой логики, а именно установить тайм-аут на все это. Вы можете сказать iOS, чтобы она выполнила метод через минуту. Если к этому времени приложение еще не нашло местоположение, вы останавливаете диспетчер местоположений и показываете сообщение об ошибке.
BOS Сначала добавьте новую переменную экземпляра.:
var timer: Timer?
Затем измените наstartLocationManager()
:
func startLocationManager() { if CLLocationManager.locationServicesEnabled() { . . . timer = Timer.scheduledTimer( timeInterval: 60, target: self, selector: #selector(didTimeOut), userInfo: nil, repeats: false) } }
Новые строки настраивают объект таймера, который отправляет didTimeOut
сообщение self
через 60 секунд; didTimeOut
это имя метода.
Селектор-это термин, который Objective-C использует для описания имени метода, а #selector()
синтаксис-это способ создания селектора в Swift.
Изменение наstopLocationManager()
:
func stopLocationManager() { if updatingLocation { . . . if let timer = timer { timer.invalidate() } } }
Вы должны отменить таймер в случае, если диспетчер местоположений будет остановлен до того, как сработает тайм-аут. Это происходит, когда достаточно точное местоположение найдено в течение одной минуты после запуска или когда пользователь нажимает кнопку Остановки.
Наконец, добавьте методdidTimeOut()
:
@objc func didTimeOut() { print("*** Time out") if location == nil { stopLocationManager() lastLocationError = NSError( domain: "MyLocationsErrorDomain", code: 1, userInfo: nil) updateLabels() } }
В этом методе есть что — то новое — раньше был новый @objc
атрибутfunc
-что бы это могло быть?
Помните, как #selector
выглядит концепция Objective-C? (Как вы могли забыть, что это было всего несколько абзацев назад, верно?) Таким образом, когда вы используете #selector
для определения метода вызов, этот метод должен быть доступен не только из Swift, но и из Objective-C. @objc
Атрибут позволяет идентифицировать метод — или класс, или свойство, или даже перечисление — как доступный из Objective-C.
Итак, это то, для чего вы сделали didTimeOut
— объявили его доступным из Objective-C.
didTimeOut()
всегда вызывается через одну минуту, независимо от того, получили ли вы действительное местоположение или нет — если stopLocationManager()
только сначала не отменит таймер.
Если после этой минуты действительного местоположения все еще нет, вы останавливаете диспетчер местоположений, создаете свой собственный код ошибки и обновляете экран.
Создавая свой собственный NSError
объект и помещая его в переменную lastLocationError
экземпляра, вам не нужно менять какую-либо логику updateLabels()
.
Однако вы должны убедиться, что домен ошибки не kCLErrorDomain
потому, что этот объект ошибки исходит не из основного местоположения, а из вашего собственного приложения.
Домен ошибок — это просто строка, так MyLocationsErrorDomain
что сойдет. Для кода я выбрал 1. На данный момент значение кода не имеет значения, потому что у вас есть только одна пользовательская ошибка, но вы можете себе представить, что когда ваше приложение станет больше, вам может понадобиться несколько кодов ошибок.
Обратите внимание, что вам не всегда нужно использовать NSError
объект; есть и другие способы сообщить остальной части вашего кода о том, что произошла ошибка. В этом случае updateLabels()
я уже использовал an NSError
в любом случае, так что наличие собственного объекта error просто имело смысл.
BOS Запустите приложение. Установите для местоположения симулятора значение None и нажмите Get My Location.
Через минуту область отладки должна сказать “*** Тайм-аут”, и кнопка «Стоп» возвращается, чтобы узнать мое местоположение. На экране также должно появиться сообщение об ошибке:

Ошибка после тайм — аута
Просто получить простое местоположение из Основного местоположения и найти соответствующий уличный адрес оказалось намного сложнее, чем казалось. Есть много разных ситуаций, с которыми нужно справляться. Ничто не гарантировано, и все может пойти не так — разработка iOS иногда требует стальных нервов!
Напомним, что приложение может либо:
- Найдите местоположение с нужной точностью,
- Найдите местоположение, которое не так точно, как вам хотелось бы, и не получает более точных показаний,
- Вообще не находит места,
- Или слишком долго искать место.
Теперь код обрабатывает все эти ситуации, но я уверен, что он еще не идеален. Без сомнения, логику можно было бы еще подправить, но для целей этой книги она подойдет.
Надеюсь, вам ясно, что если вы выпускаете приложение, основанное на местоположении, вам нужно провести много полевых испытаний!
Необходимые возможности устройства
Вкладка Информация может содержать ключ «Необходимые возможности устройства«, в котором перечислены аппаратные средства, необходимые вашему приложению для запуска. Это ключ, который App Store использует для определения того, может ли пользователь установить ваше приложение на свое устройство.
Если вашему приложению требуются дополнительные функции, такие как Основное местоположение для получения местоположения пользователя, вы должны перечислить их здесь.
- Выберите вкладку Информация, добавьте новую строку и установите ключ в соответствии с требуемыми возможностями устройства.
Значение нового ключа — это массив. Если вы развернете список элементов массива, то заметите, что по умолчанию он пуст.
BOS Щелкните двуглавую стрелку в конце поля значение для пустого элемента массива, чтобы получить список возможных значений. Выберите Службы определения местоположения из списка:

Добавление геолокационных сервисов в Info.plist
Вы также можете добавить GPS, чтобы приложение требовало GPS — приемника. Но если вы это сделаете, пользователи не смогут установить приложение на iPod touch или на некоторые iPad.
P.S. Теперь вы можете вынуть print()
утверждения из приложения или просто прокомментировать их. Лично мне нравится держать их там, так как они удобны для отладки. В приложении, которое вы планируете загрузить в App Store, вы обязательно захотите удалить print()
операторы по завершении разработки.

Атрибуты и свойства
Большинство атрибутов в инспекторах Interface Builder непосредственно соответствуют свойствам выбранного объекта. Например, a UILabel
имеет следующие атрибуты:

Они непосредственно связаны со следующими свойствами:

И так далее… Как вы можете видеть, имена не всегда могут быть точно такими же (“Строки” иnumberOfLines
), но вы можете легко выяснить, какое свойство сочетается с каким атрибутом.
Вы можете найти эти свойства в документации для UILabel
. В меню Справка Xcode выберите пункт Документация разработчика. Введите “uilabel” в поле поиска, чтобы открыть ссылку на класс для UILabel
:

В документации для UILabel
не перечислены свойства для всех атрибутов инспекторов. Например, в инспекторе атрибутов есть раздел с именем “Вид”. Атрибуты в этом разделе исходят из UIView
того , что является базовым классом UILabel
. Поэтому, если вы не можете найти свойство в UILabel
классе, вам может потребоваться проверить документацию в разделе “Наследуется от”, который находится в разделе **»Отношения **UILabel
» документации.
