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

в 10:13, , рубрики: iOS, ios development, iOS разработка, разработка под iOS, Яндекс API

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

Совсем недавно я опубликовал статью, буквально пропитанную любовью к Яндекс.Картам. Поэму. Оду. Вот, собственно, она 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) встраиваемых модулей карт.

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

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

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

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

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

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

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

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

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

@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» — сказал я сам себе. Как оказалось далее — действительно, такой «друг» есть в документации… НО ГДЕ ПРИМЕР С НИМ?!?
В примерном проекте примера применения такого менеджера нет:
Яндекс.Карты: Зашел на контроллер карт — сразу получил позицию пользователя (окей, ну а теперь серьезно) - 6

— Ну ладно, документация, остались только мы с тобой.
— Да нет проблем, «новатор», наслаждайся:
Яндекс.Карты: Зашел на контроллер карт — сразу получил позицию пользователя (окей, ну а теперь серьезно) - 7

Подписываем 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) {}
}

И…

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

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

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

Подумал еще… и следующая эмоция была такой:
Яндекс.Карты: Зашел на контроллер карт — сразу получил позицию пользователя (окей, ну а теперь серьезно) - 9
Кто хочет со звуком — www.youtube.com/watch?v=pTZaNHZGsQo

Рецепт успеха был такой:
Берем кило ... CLLocationManager и YMKLocationManager и… заставляем их работать вместе.
Яндекс.Карты: Зашел на контроллер карт — сразу получил позицию пользователя (окей, ну а теперь серьезно) - 10

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

// 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 секунд.

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

Геокодер*

В качестве бонуса покажу реализацию геокодера.
Использовал Геокодер Яндекса 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

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

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

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

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

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

Автор: Serge Vikhlyaev

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js