Снова приветствую!
Совсем недавно я опубликовал статью, буквально пропитанную любовью к Яндекс.Картам. Поэму. Оду. Вот, собственно, она habr.com/ru/post/479102
Удостоверившись, что среди программистов мало любителей стихов, я все же решил осветить ситуацию более «по-ХАБРовски». Ловите кучку кода, размышлений и скринов. Поехали.
Начнем сначала.
Задача тривиальна: при входе на контроллер с картами нужно сразу «отзуммиться» на точку пользователя (бонус: надо бы еще получить адрес в читаемом виде вместе со всеми доступными атибутами).
Врубаем аналитика: «Разделяй и властвуй».
Для достижения поставленной цели необходимо решить ряд технических и бизнес-задач, а именно:
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)
}
В делегат мы отправляем управляющую часть вызывающего кода. Тут как бы все понятно.
Вот так незатейливо мы переходим на контроллер…
Перекрестие в центре находится точно в центре контроллера и в центре 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
Результат работы визуально такой:
Резюмируя вышесказанное:
Коллеги, статья написана для того, чтобы при решении подобной задачи вы не тратили столько же времени, сколько потратил я и либо выбрали другой путь, либо прошли его быстро.
Хотелось бы увидеть конструктивную критику и/или альтернативные правильные решения.
Если статья оказалась для вас полезной — поправьте мне рейтинг лойсом и, желательно, зацените по-новому первоначальный вариант этого повествования habr.com/ru/post/479102
Всем творческих успехов и положительного настроения!
Автор: Serge Vikhlyaev