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

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

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

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



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

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

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

    Всем творческих успехов и положительного настроения!
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое