Хабрахабр

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

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

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

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

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

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

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

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

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

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

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

extension AddPresenter: AddPresentationLogic // ...
}

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

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

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

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

image

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

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

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

@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) {}
}

И…

15! 1-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

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

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

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

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

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

Теги
Показать больше

Похожие статьи

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

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

Кнопка «Наверх»
Закрыть