Маршрутизация с MapKit и Core Location
Маршрутизация с MapKit и Core Location
Apple внесла основные обновления для фреймворков MapKit и CoreLocation на iOS 9, а именно, более подробные карты, новые возможности транзитной маршрутизации и упрощенная система поиска локализации. Стремление Apple обогнать своих конкурентов (а именно Google Maps) должно быть достаточным стимулом присоединиться к стремительно развивающимся Apple Maps, если вы еще этого не сделали!
В этом уроке вы создадите приложение с именем Отмщение Прокрастинатора, которое поможет вам найти самый быстрый маршрут «туда-обратно» от начальной точки до двух точек назначения и обратно. Приложение получает данные, используя CoreLocation, и затем находит самый быстрый маршрут между этими адресами, используя MapKit.
Приступаем
Загрузите стартовый проект и откройте ProcrastinatorsRevenge.xcodeproj в Xcode. Запустите его, и чтобы понять, как оно работает, пощелкайте внутри приложения.
Первый экран приложения имеет три текстовых поля — одно для исходного / конечного адреса и два поля для промежуточных остановок. Нажмите на Route It!, и приложение перейдет на второй экран с картой.
Использование MapKit с CoreLocation
Как именно MapKit относится к CoreLocation?
В документации Apple просто говорится «Фреймворк Core Location позволяет определить текущее местоположение» и, хотя вы будете использовать эту особенность CoreLocation для предварительного заполнения отправной точки пользователя, возможность CoreLocation изменять координаты и частично адреса в удобные адресные отображения объектов на карте, используя свой класс CLGeocoder, будет иметь огромное значение для завершения первой части этого урока.
Во второй части урока, вы будете конвертировать CLPlacemark, возвращенный из CLGeocoder в MKPlacemark, и, в свою очередь, конвертировать этот MKPlacemark в MKMapItem. Затем Вы сможете использовать MKMapItem для запуска MKDirectionsRequest который, наконец, возвратить данные MKRoute от Apple.
Таким образом, резюмируем: CLGeocoder > CLPlacemark > MKPlacemark > MKMapItem > MKDirectionsRequest > MKRoute.
Это может показаться сложным, но, к счастью для вас, ваша давно потерянная Сумасшедшая тетя Люси дала вам отличную мотивацию, чтобы начать!
Охота на миллионы тети Люси
После того, как вы целую жизнь верили, что тетя Люси это больше, чем миф, который ваши родители придумали, чтобы оградить вас от возможных последствий чрезмерного употребления наркотиков, неожиданно появляется письмо по почте от самой легенды:
Перевод: Простите великодушно за мое долгое отсутствие. Я много путешествовала по суше, морю и пространству, собирая сокровища мира- в том числе и зуб мудрости Лохнесского чудовища и большую коллекцию ногтей инопланетян. Я накопила $ 500000000, ошибочно приняв 10 тонн золотых самородков за экскременты эльфов… впрочем последние даже лучше продаются на сегодняшний день…
Вас сердечно приглашают на 105 Any Way, озеро Jackson, Texas для того, чтобы принять участие в соревновании за долю моего наследства. Вы будете соревноваться в короткой гонке по уборке мусора в лабиринтах нашего города и кто придет первый с трофеями, тот и получит долю моего наследства.
Искренне ваша (до подписания силы не имеет)
Ой-ой. Ты то известен, как вечно опаздывающий среди своих неприятно пунктуальных двоюродных братьев. Но это приложение станет вашем шансом, чтобы отомстить и забрать миллионы тети Люси себе!
Перво-наперво: вы упростите задачу для пользователей и сделаете возможным заполнение поля начальной / конечной позиции текущим адресом пользователя.
Получение текущего адреса с помощью CoreLocation
В ViewController.swift, добавьте следующий код, заменив существующий viewDidLoad, для настройки и создания экземпляра объекта CLLocationManager:
Рассмотрим все пронумерованные секции по порядку:
- Вы объявляете locationManager как глобальный экземпляр, чтобы поддерживать сильную ссылку.
- В viewDidLoad, после того как вы установили делегата location менеджера, то вы явно просите разрешения на доступ местоположение пользователя, когда приложение используется. Этот сигнал больше не будет отображаться при последующем запуске приложения, как только пользователь выберет ответ.
- После активации системы определения местоположения, установите нужную точность определения местонахождения CLLocationManager, затем запросите текущее местоположение, используя .requestLocation() (представлена в iOS 9).
Запустите ваше приложение. Получили уведомление с запросом на авторизацию? Нет?
Это потому, что осталась еще одна вещь, о которой вам нужно позаботиться. Вы должны предоставить пользователю причину для вашего запроса.
Откройте Supporting Files > info.plist и выполните следующие действия:
- Добавьте «NSLocationWhenInUseUsageDescription» в качестве ключа в Information Property List.
- Оставьте Type как String.
- Установите Value сообщение, для того, чтобы объяснить пользователю, почему вы просите их разрешить определить его позицию: «Позвольте нам получить доступ к вашему текущему местоположению, чтобы мы могли автоматически заполнить ваш исходный / конечный пункт назначения».
Заметка
NSLocationWhenInUseUsageDescription | .requestWhenInUseAuthorization() позволяет приложению иметь доступ к местоположению пользователя в то время как используется приложение.
NSLocationAlwaysUsageDescription | .requestAlwaysAuthorization() позволяет приложению получить доступ к местоположению пользователя, даже в то время как приложение в фоновом режиме.
Запустите приложение снова. Теперь уведомление должно появиться, как и ожидалось:
Нажмите Allow (Разрешить). Теперь менеджер позиции (location manager) знает ваше местоположение в настоящее время.
Далее создайте объект CLGeocoder для reverse geocode (обратного геокода) текущего CLLocationManager’s CLLocation. Обратное геокодирование- процесс превращения координат location в читабельный адрес.
Прокрутите вниз ViewController.swift и добавьте следующий код для locationManager(_:didUpdateLocations:locations:):
reverseGeocodeLocation(_:completionHandler:) возвращает массив меток (placemarks) в его обработчик по завершению. Для большинства результатов геокодирования, этот массив будет содержать только один элемент; в редких случаях, одно location может возвратить несколько близлежащих местоположений. В этом случае, любого из этих местоположений, возможно placemarks[0], должно быть достаточно. Вы также можете остановить обновление местоположения, если уже нашли подходящую метку.
Теперь, когда вы нашли CLPlacemark, отображающую текущий адрес пользователя, вам нужно связать данные о местоположении с соответствующим текстовым полем. Чтобы сделать это, используйте структуру кортежа для группировки нескольких значений в одно составное значение.
Прямо над viewDidLoad добавьте следующую глобальную переменную для связи каждого UITextField с ему соответствующим MKMapItem:
Вы будете хранить MKMapItems (не CLPlacemarks) для местоположения пользователя, так как это тип объекта, который вы в конечном итоге будете использовать для инициализации MKDirectionsRequest необходимого для расчета маршрута. В viewDidLoad добавьте:
Здесь вы предварительно заполняете массив кортежами, каждый из которых содержит текстовое поле и nil значение в MKMapItem, что в конечном итоге может быть связано с этим текстовым полем.
Этот массив является структурой данных location, которая будет служить вам верой и правдой до конца этого урока.
Прокрутите вниз до locationManager(_:didUpdateLocations:location:) и добавьте следующий фрагмент после объявления placemark внутри обработчика по завершению reverseGeocodeLocation(_:completionHandler:):
Это добавит отображение MKMapItem текущего местоположения пользователя в первый кортеж locationTuples.
Затем добавьте следующую функцию основному классу ViewController (не расширение), чтобы преобразовать данные местоположения в читаемый адрес:
formatAddressFromPlacemark(_:) принимает строку за строкой массив адреса, хранимый в ключе «FormattedAddressLines» адресного словаря CLPlacemark, а затем объединяет содержание с запятыми между каждым элементом.
Вернитесь к locationManager(_:didUpdateLocations:locations:) после добавленной инициализации self.locationTuples[0].mapItem и добавьте после нее:
У UITextField появляется новый адрес.
Внутри того же блока if let, добавьте следующее:
В начальном проекте, выделенный текст кнопок уже предварительно установлен, теги полей и теги кнопок назначены в цифровой последовательности и кнопки Enter все связаны с IBOutletCollection enterButtonArray помощью Interface Builder. Приведенный выше код находит и выбирает кнопку Enter с тегом 1, то есть кнопку Enter следующую от источника UITextField также с тегом 1, так что текст кнопки изменяется на ✓, чтобы отразить его выбранное состояние.
Запустите ваше приложение на симуляторе. Предположим, что Apple HQ будет дефолтным текущим местоположением, то ваше текстовое поле должно содержать “Apple Inc., 2 Infinite Loop, Cupertino, CA 95014-2083, United States”:
Пришло время внести изменения в адрес Сумасшедшей тети Люси!
Щелкните в любом месте в пределах симулятора для раскрытия панели меню, выберите Debug > Location > Custom location…:
Введите координаты тети Люси:
Запустите еще раз приложение. Теперь должно появится следующее: «105 Any Way St, Lake Jackson, TX, 77566-4198, United States» в поле «исходный/конечный» адрес
Далее, нам нужно скорректировать вводимую пользователем информацию для того, чтобы адрес отображался полностью и точно и создания MKMapItems этих адресов, чтобы связать их с соответствующими текстовыми полями.
Обработка ввода значений пользователем через CoreLocation
Продолжаем в ViewController.swift, обновим addressEntered(_:) следующим образом:
Вот то, что вы добавили:
- В Interface Builder каждой кнопке Enter был дан тег, соответствующий ее порядковому номеру сверху вниз: 1, 2 и 3, соответственно. Вы можете использовать sender.tag для того, чтобы найти соответствующее текстовое поле.
- Передаем geocode адрес, используя метод CLGeocodergeocodeAddressString(_:completionHandler:).
В отличие от reverseGeocodeLocation(_:completionHandler:), geocodeAddressString(_:completionHandler:) может часто возвращать более одного CLPlacemark поскольку вводимый текст часто не дает точного совпадения в одним location. К счастью, мы уже создали подкласс UITableView, для того чтобы позволить пользователю выбрать адрес из возвращенных CLPlacemarks.
Взгляните на AddressTableView.swift. Как должно быть ясно из tableView(_:numberOfRowsInSection:) и tableView(_:cellForRowAtIndexPath:) вы будете использовать массив addresses, объявленный в верхней части класса в глобальной переменной для заполнения таблицы. Добавьте следующую функцию в класс ViewController:
Здесь вы создаете AddressTable и установливаете его массив addresses с помощью CLPlacemarks, возвращенный geocodeAddressString(_:completionHandler:).
Вернемся к addressEntered(_:).Внутри блока if let placemarks = placemarks в обработчике по завершению geocodeAddressString(_:completionHandler:) добавляем:
Создаем маршрут, соединяя placemarks, заполняем новый массив адресов Strings, а затем передаем их вместе в showAddressTable(_:).
Запускаем приложение. Взгляните на подсказку тети Люси Clue # 1, чтобы выяснить, какой адрес ввести в поле “Stop # 1”:
Перевод: Ключ к разгадке #1. Между этой дорогой, той дорогой и какой-либо другой дорогой. Ехала обезьяна с хорьком. Обезьяна захотела на завтрак булочку с кунжутом. “Я тоже, ХЛОП!” сказал хорек.
Хм . Эта дорога, та дорога, и какая-либо другая — так можно сказать о всех улицах в Лейк-Джексоне (что достаточно смущает). Вы ищете сендвич на завтрак в ресторанчике, проезжая где-то между. Эта подсказка звучит очень похоже на песню «Pop! Идет Хорек » (Pop! Goes the Weasel). «Pop! Goes the Weasel » эта мелодия часто бывает в коробочках с выпрыгивающими попрыгунчиками (примеч. jack-in-the-box) . вот оно что! Location # 1 Закусочная “Jack in the Box” по адресу 165 Oyster Creek Dr.
Печатаем «165 Oyster Creek Dr, Lake Jackson, TX»; появляется таблица и полный адрес в качестве опции:
Что произойдет, когда вы выберите адрес? Ничего особенного! :] Таблица исчезнет, но адрес остается прежним. Пришло время это изменить.
При выборе строки, содержащей адрес, вы хотите чтобы автоматически установленное соответствующее текстовое поле, содержало выбранный адрес, обновите массив locations, чтобы он содержал соответствующий MKMapItem и сделайте соответствующую кнопку Enter выделенной.
Обновите showAddressTable(_:) в ViewController.swift таким образом:
Здесь вы переносите AddressTableView текущее поле текст, метки, указатель на текущий экземпляр ViewController.swift так что вы можете легко изменить его массив locationTuples и кнопки Enter.
Внутри geocodeAddressString(_:completionHandler:), обновите вызов showAddressTable(_:), чтобы он прошел соответствующие параметры:
Затем в блоке else, который следует непосредственно после вызова showAddressTable: добавьте уведомление:
Если geocodeAddressString(_:completionHandler:) не возвращает никаких placemarks, выйдет ошибка.
Затем добавьте следующее в tableView(_:didSelectRowAtIndexPath:) в AddressTable.swift’s:
Давайте рассмотрим, что происходит по порядку:
- Поскольку последняя строка в таблице это «Ни один из вышеперечисленных,” вам только нужно обновить текстовое поле и связанный с ним объект на карте, когда строка меньше, чем длина массива addresses.
- Обновите текущее текстовое поле, чтобы оно содержало выбранный адрес.
- Создайте MKMapItem с placemark, соответствующей выбранной строке и связывающей MKMapItem с текущим текстовым полем в массиве mainViewControllerlocationTuples.
- Выберите текущую кнопку Enter.
Запустите приложение. Введите адрес в поле Stop # 1 и нажмите Enter, затем выберите правильный адрес в таблице. Текстовое поле, массив кортежа, и кнопка Enter должна обновиться соответсвенно:
Итак с ViewController.swift еще не закончили! Осталось еще несколько нерешенных задач.
Обновите textField(_:shouldChangeCharactersInRange:replacementString:):
Когда пользователь редактирует поле, вы аннулируете MKMapItem, поскольку он не может больше применяться и вам нужно отменить соответствующую кнопку Enter, чтобы пользователь знал, что он должен будет повторно выбрать правильный адрес.
Затем, обновите swapFields(_:) следующим образом:
Когда пользователь нажимает «↑ ↓», вы должны поменять текст, который MKMapItems содержит в индексах 1 и 2 locationTuples и выбранные состояния 2 и 3 кнопок Enter.
Наконец, нужно подготовить приложение для перехода в DirectionsViewController, где вы будете рассчитывать маршрут с помощью переопределения нескольких методов класса NSSeguePerforming.
В основном классе ViewController (как вариант, под методом getDirections(_:)) запишите метод shouldPerformSegueWithIdentifier(_:sender:)
Условные выражения if-else предотвращает переход, если исходное положение и хотя бы один из пунктов назначения не установлены.
Подготовьте массив locationTuples для следующего вида, добавив следующее только-для-чтения, вычисляемое свойство над viewDidLoad:
locationsArray отфильтровывает индексы locationTuples, содержащие nil MKMapItems, и так как приложение будет выдавать маршрут туда-обратно, то filtered += [filtered.first!] копирует кортеж на первом индексе до конца массива. Под shouldPerformSegueWithIdentifier(_:sender:), переопределите prepareForSegue(_:sender:):
Это перенесет locationsArray на следующий view controller.
Ну вот и все в ViewController.swift! Теперь переключитесь на DirectionsViewController.swift и начнем прокладывать маршрут.
Рассчитываем маршрут с MapKit
Подсказка от тетушки Люси #2
Перевод: После ночи с Радербергером (название пива) и Рамштаймом. Мне нужно болеутоляющее, чтобы убить мою жажду. Вы найдете и того и другого либо на этом пути, либо на том. Не в лучшем доме, а в _________.
Немецкие флюиды . обезболивающее . опять упоминание о “этот путь и то . «Не в лучшем доме, а в ___» . Хуже? Это, кажется, подходит . И вот! На углу есть немецкая пивнушка, которая называется Wurst Haus, где подают напиток под названием Обездоливающее! Это по адресу 102 This Way Lake Jackson, TX.
Теперь вы знаете все адреса, и вам нужно создать объект MKDirections, а затем вызвать его calculateDirectionsWithCompletionHandler(_:) для источника и направления каждого сегмента для того, чтобы рассчитать маршрут.
Добавьте следующее в DirectionsViewController:
Вот то, что мы сделали:
- Создали MKDirectionsRequest, установив MKMapItem в данный индекс locationArray в качестве источника и установки MKMapItem на следующий индекс в качестве пункта назначения.
- Установили requestsAlternateRoutes как true, чтобы извлечь все подходящие маршруты от источника к месту назначения.
- Установили тип транспорта для .Automobile для данного сценария. (.Walking (пешком) И .Any также доступные MKDirectionsTransportTypes.)
- Инициализировали объект MKDirections с MKDirectionsRequest, а затем вызвали calculateDirectionsWithCompletionHandler(_:), чтобы получить MKDirectionsResponse, содержащий массив MKRoutes.
Если calculateDirectionsWithCompletionHandler(_:) не возвращает никаких маршрутов, а вместо этого возвращает ошибку, то будет выполнен блок else if let _ = error. Добавьте следующее в логический блок else if:
Здесь происходит вывод сообщения об ошибке и возврат пользователя к предыдущему view controller.
Предполагаемые MKRoutes найдены, первое выражение if let внутри calculateDirectionsWithCompletionHandler(_:) будет выполнено как true. В этом блоке if let добавьте:
Здесь вы сортируете маршруты от наименьшего к наибольшему ожидаемому времени путешествия, затем вытащите первый индекс, т.е. индекс с наименьшим ожидаемым временем путешествия. Это даст вам самый быстрый маршрут между двумя точками. Вы на пути к победе!
Но вам все еще нужно рассчитать несколько маршрутов между несколькими точками. Вы можете сделать это рекурсивно. Во-первых, обновите параметры calculateSegmentDirections(_:):
calculateSegmentDirections(_:time:routes:) теперь принимает изменяемые массивы сегменты маршрутов и изменяемые переменные времени.
Затем внутри первого блока if let и после объявления quickestRouteForSegment добавьте:
- Добавили самый быстрый маршрут для этого текущего сегмента массива routes, переданного в качестве параметра.
- Добавили предполагаемое время в пути в параметр time.
- Так как вы пока не достигли двух окончательных значений массива location, рекурсивный вызов calculateSegmentDirections(_:time:routes:) с увеличенным индексом и обновленными значениями времени и маршрута.
Теперь вернемся к viewDidLoad и добавим следующее:
Этот код добавляет индикатор активности, в момент когда рассчитывается маршрут, затем вызывается calculateSegmentDirections(_:time:routes:) для расчета маршрута, начиная с индекса 0 для locationArray, с начальным общим временем 0 и первоначально пустым массивом маршрута.
Вернемся к calculateDirectionsWithCompletionHandler(_:). Внутри блока else, следующего непосредственно за if index+2
SwiftBook
Самое крупное сообщество iOS разработчиков на языке Swift
Источник