Яндекс.Карты: Зашел на контроллер карт — сразу получил позицию пользователя (окей, ну а теперь серьезно)

от автора

Снова приветствую!

Совсем недавно я опубликовал статью, буквально пропитанную любовью к Яндекс.Картам. Поэму. Оду. Вот, собственно, она habr.com/ru/post/479102

Удостоверившись, что среди программистов мало любителей стихов, я все же решил осветить ситуацию более «по-ХАБРовски». Ловите кучку кода, размышлений и скринов. Поехали.

image

Начнем сначала.

Задача тривиальна: при входе на контроллер с картами нужно сразу «отзуммиться» на точку пользователя (бонус: надо бы еще получить адрес в читаемом виде вместе со всеми доступными атибутами).

Врубаем аналитика: «Разделяй и властвуй».

Для достижения поставленной цели необходимо решить ряд технических и бизнес-задач, а именно:

0. Перейти на контроллер с потенциальным модулем работы с геопозицией (МРСГ), callBack-ами и т.д.

1. МРСГ
1.1 Реализовать МРСГ
1.2 Запустить МРСГ
1.2.1 Получить координаты пользователя
1.2.2 Отзуммиться на него
2*. Получить адрес позиции в читаемом формате.

Переход на контроллер (VIPER + Configurator)

extension AddPresenter: AddPresentationLogic {     // ...     func openMap(_ delegate: YandexMapSetPointViewControllerProtocol?) {            router.routeTo(target: .addPlace(delegate))      }     // ... }

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

protocol YandexMapSetPointViewControllerProtocol {     func didSelectPoint(_ place: Place) }

В делегат мы отправляем управляющую часть вызывающего кода. Тут как бы все понятно.

Вот так незатейливо мы переходим на контроллер…

image

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

Левее и чуть ниже перекрестия — UILabel. Туда планируется выводить читаемы адрес. Справа кнопка с UIActivityIndicator. Смысл в том, что, пока координаты пользователя не пришли, он «вращается» и кнопка затемнена и disabled. По нажатию на кнопку с полученными координатами пользователя мы возвращаем перекрестие на него. Этакое указание позиции, отталкиваясь от позиции пользователя.

Внизу кнопка «Выбрать точку». По нажатию происходит магия бизнес-логики:

@IBAction func selectButtonWasPressed(_ sender: Any) {         let place = Place()         place.name = "Указанная геопозиция"         place.point.latitude = "\(String(describing: selectedPoint!.latitude))"         place.point.longitude = "\(String(describing: selectedPoint!.longitude))"         place.addressText = selectedPointGeocoderedAddress                  delegate?.didSelectPoint(place)         navigationController?.popViewController(animated: true)     } 

Ура! Мы обсудили подготовительный этап!

Приступаем к МРСГ.

Ниже приведен табличнообразно-отформатированный текст, отражающий лично мои оценки (с элементами нечеткости) самых известных (на тот момент я не знал про www.openstreetmap.org, спасибо, daglob) встраиваемых модулей карт.

«Так как задача простая, использую-ка Яндекс.Карты. Они красивые, шустрые…» — подумал я.

Если кому интересно, как это настраивать, — напишите минипроект, но, если лень — tech.yandex.ru/maps/mapkit/?from=mapsapi, вставай на рельсы моего пути. Начать несложно.

Факт в том, что документация представлена в таком виде:

Обратите внимание на скудное описание и огромный список объектов слева. Чёрт ногу сломит.
«Наверное, тестовый проект ответит на мои вопросы». Ну ну.

Вот этот зверь. github.com/yandex/mapkit-ios-demo
Решения для своей тривиальной задачи я там не увидел.
— Ладно, — думаю — опыта мне хватит, я ли не разраб.

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

Ключевые моменты:
Есть объект:

@IBOutlet weak var mapView: YMKMapView! // и YMKUserLocationObjectListener - слушатель, получающий сведения

«Вроде бы все логично» скажете вы. Ан нет. Проимплементив методы:

    func onObjectAdded(with view: YMKUserLocationView) {}      func onObjectRemoved(with view: YMKUserLocationView) {}      func onObjectUpdated(with view: YMKUserLocationView, event: YMKObjectEvent) {}

мы получаем возможность добраться к point.lat и point.long крайне непростыми путями.
Например, вот так:

userLocation = YMKPoint(latitude: view.pin.geometry.latitude, longitude: view.pin.geometry.longitude)

Время ожидания подгрузки координат варьируется при таком подходе от 2 до 50 секунд.

«Ты неправ, там должен быть целенаправленный LocationManager» — сказал я сам себе. Как оказалось далее — действительно, такой «друг» есть в документации… НО ГДЕ ПРИМЕР С НИМ?!?
В примерном проекте примера применения такого менеджера нет:

— Ну ладно, документация, остались только мы с тобой.
— Да нет проблем, «новатор», наслаждайся:

Подписываем UIViewController на протокол (надеюсь, не надо тут ничего дополнительно пояснять, ну реально, ребяяяят):

// MARK: - // Params var userLocation: YMKPoint? {         didSet {             guard userLocation != nil && userLocation?.latitude != 0 && userLocation?.longitude != 0 else { return }                          if isItFirstSelection {                 isItFirstSelection = false                 selectedPoint = userLocation                                  mapView.mapWindow.map.move(                     with: YMKCameraPosition.init(target: userLocation!, zoom: 16, azimuth: 0, tilt: 0),                     animationType: YMKAnimation(type: YMKAnimationType.smooth, duration: 1),                     cameraCallback: nil)             }                          activityIndicator.stopAnimating()         }     }  // MARK: - // Some like didLoad setupLocationManager()  // MARK: - // Setup private func setupLocationManager() {         locationManager = YMKMapKit.sharedInstance()!.createLocationManager()         locationManager.subscribeForLocationUpdates(withDesiredAccuracy: 0, minTime: 10, minDistance: 0, allowUseInBackground: true, filteringMode: .on, locationListener: self)              }  // MARK: - // MARK: YMKLocationDelegate extension YandexMapSetPointViewController: YMKLocationDelegate {     func onLocationUpdated(with location: YMKLocation) {         userLocation = YMKPoint(latitude: location.position.latitude, longitude: location.position.longitude)     }          func onLocationStatusUpdated(with status: YMKLocationStatus) {} } 

И…

1-15 секунд, КАРЛ! 15! Иногда быстрее отрабатывает предыдущий вариант! Как так-то??
Яндекс, ну что за прикол? Столько времени потратить, чтобы это все попробовать, так еще и получить такой результат — ну это вообще грустно.

Думал я, думал… Ну не реально же. Дать человеку контроллер с картами и ввести его в ступор при переходе на него более, чем на 4 секунды — это самоубийство для приложения. Никто не будет ждать больше 5 секунд с полной уверенностью, что это комфортно (если не верите мне — послушайте доклады Виталия Фридмана по UI/UX).

Подумал еще… и следующая эмоция была такой:


Кто хочет со звуком — www.youtube.com/watch?v=pTZaNHZGsQo

Рецепт успеха был такой:
Берем кило … CLLocationManager и YMKLocationManager и… заставляем их работать вместе.

Выглядит эта совместная… «работа» примерно так:

// Params private var locationManager: YMKLocationManager!     private var nativeLocationManager = CLLocationManager()  // MARK: - // Some like didLoad setupNativeLocationManager()  // MARK: - // Setup private func setupNativeLocationManager() {         if CLLocationManager.locationServicesEnabled() {             nativeLocationManager.delegate = self             nativeLocationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters             nativeLocationManager.startUpdatingLocation()         }     }  // MARK: - // CLLocationManagerDelegate extension YandexMapSetPointViewController: CLLocationManagerDelegate {     func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {         userLocation = YMKPoint(latitude: locations.last!.coordinate.latitude, longitude: locations.last!.coordinate.longitude)     } } 

… и плов готов

Результат скорости получения точки пользователя: чуть больше, чем 0 секунд.

Тот самый случай, когда (на мой взгляд) при решении тривиальной задачи противостояние нативной и встраиваемой части выглядело так:

Геокодер*

В качестве бонуса покажу реализацию геокодера.
Использовал Геокодер Яндекса tech.yandex.ru/maps/geocoder

// Params private let geocoderManager = GeocoderManager.shared  // Не стал шерстить импорт на избыточность. Вроде бы все нужно import Foundation import Alamofire import Alamofire_SwiftyJSON import SwiftyJSON import PromiseKit import UIKit  // MARK: - // MARK: GeocoderManager class GeocoderManager {     static let shared = GeocoderManager()     private init() {}          func getAddressBy(latitude: String, longitude: String, completion: @escaping (Bool, String?, Error?)->()) {         GeocoderAPI().request(latitude: latitude, longitude: longitude).done { (response) in             completion(true, response.getAddress(), nil)             print("success")         }.catch { (error) in             completion(false, nil, error)         }     } }  //  import Foundation import Alamofire import PromiseKit  // MARK: - // MARK: Request enum GeocoderRequest {     case addressRequest(String, String) }  // MARK: - // MARK: GeocoderRequest extension GeocoderRequest: DefaultRequest {     var path: String {         switch self {         case .addressRequest:             return "1.x/"         }     }          var method: HTTPMethod {         switch self {         case .addressRequest:             return .get         }     }          var headers: HTTPHeaders {         return [:]     }          var parameters: [String: Any]? {         switch self {         case .addressRequest(let latitude, let longitude):             return [                 "apikey"    : Consts.APIKeys.yandexGeocoderKey,                 "format"    : "json",                 "results"   : 1,                 "spn"       : "3.552069,2.400552",                 "geocode"   : "\(longitude),\(latitude)"             ]         }     }           func asURLRequest() throws -> URLRequest {         let url = try GlobalConsts.Links.geocoderBaseURL.asURL()// not good, need new idea for this         var urlRequest = URLRequest(url: url.appendingPathComponent(path))         urlRequest.httpMethod = method.rawValue         urlRequest.allHTTPHeaderFields = headers         switch method {         case .get:             urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters)         case .post:             urlRequest = try JSONEncoding.default.encode(urlRequest, with: parameters)         case .put:             urlRequest = try JSONEncoding.default.encode(urlRequest, with: parameters)         case .patch:             urlRequest = try JSONEncoding.default.encode(urlRequest, with: parameters)         case .delete:             urlRequest = try JSONEncoding.default.encode(urlRequest, with: parameters)         default:             break         }         return urlRequest     } } 

Для преобразования ответа от сервера в модель очень помогает этот ресурс: app.quicktype.io

Результат работы визуально такой:

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

Хотелось бы увидеть конструктивную критику и/или альтернативные правильные решения.

Если статья оказалась для вас полезной — поправьте мне рейтинг лойсом и, желательно, зацените по-новому первоначальный вариант этого повествования.

Всем творческих успехов и положительного настроения!


ссылка на оригинал статьи https://habr.com/ru/post/479432/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *