Yandex.Maps: I went to the card controller - I immediately got the user's position (okay, now seriously)

Greetings again!

Most recently, I published an article literally saturated with love for Yandex.Maps. The poem. Ode. Here, in fact, she habr.com/en/post/479102

Having made sure that among programmers there are few lovers of poetry, I nevertheless decided to illuminate the situation in a more “HABRovsky” way. Catch a bunch of code, thoughts and screenshots. Go.

image

Start over.

The task is trivial: when you enter the controller with the cards, you need to immediately “zoom out” to the user's point (bonus: you would also need to get the address in a readable form along with all available atibutes).

We cut the analyst: "Divide and conquer."

To achieve this goal, it is necessary to solve a number of technical and business problems, namely:

0. Go to the controller with a potential module for working with geo-positioning (MRSG), callbacks, etc.

1. IWG
1.1 Implement the IWG
1.2 Launch IWG
1.2.1 Get user coordinates
1.2.2 Take a look at him
2 *. Get the position address in a readable format.

Switching to the controller (VIPER + Configurator)

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

The delegate has only one function so that when you specify the desired point, you can return it via protocol to the controller that calls the controller with the card:

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

To the delegate, we send the control part of the calling code. Everything seems to be clear here.

This is how unpretentious we go to the controller ...

image

The crosshair in the center is located exactly in the center of the controller and in the center of the UIView maps. The assumption that zooming inside maps will work by default in the center of the window. And so it turned out.

To the left and just below the crosshairs - UILabel. It is planned to display a readable address there. Right button with UIActivityIndicator. The point is that until the user’s coordinates have arrived, he “rotates” and the button is darkened and disabled. By clicking on the button with the received coordinates of the user, we return the crosshair to him. This is an indication of the position, starting from the user's position.

At the bottom is the "Select Point" button. By clicking, the magic of business logic occurs:

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

Hurrah! We discussed the preparatory phase!

We proceed to the MRSG.

Below is a table-formatted text that personally reflects my assessments (with fuzzy elements) of the most famous (at that time I did not know about www.openstreetmap.org , thanks, daglob ) built-in map modules.



“Since the task is simple, I use Yandex.Maps. They are beautiful, nimble ... "- I thought.

If you are interested in how to configure this, write a mini-project, but if you are too lazy - tech.yandex.ru/maps/mapkit/?from=mapsapi , get on the track of my path. Getting started is easy.

The fact is that the documentation is presented in this form:



Pay attention to the meager description and the huge list of objects on the left. Damn your leg.
“Probably the test project will answer my questions.” Oh well.

Here is this beast. github.com/yandex/mapkit-ios-demo
I did not see solutions for my trivial task there.
- Okay, - I think - I have enough experience, if I’m not a developer.



I assembled a test project and looked at the feature of customizing the user's marker for a long time.

Key points:
There is an object:

 @IBOutlet weak var mapView: YMKMapView! //  YMKUserLocationObjectListener - ,   

“Everything seems to be logical,” you say. But no. Alternative methods:

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

we get the opportunity to get to point.lat and point.long in extremely difficult ways.
For example, like this:

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

The waiting time for loading coordinates varies with this approach from 2 to 50 seconds.

“You're wrong, there must be a focused LocationManager,” I told myself. As it turned out later - indeed, such a “friend” is in the documentation ... BUT WHERE IS AN EXAMPLE WITH HIM?!?
In the sample project, there is no example of the application of such a manager:



- Well, documentation, only you and I are left.
- Yes, there are no problems, "innovator", enjoy:



We subscribe the UIViewController to the protocol (I hope there is no need to further explain anything here, well, really, guys):

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

AND…



1-15 seconds, CARL! fifteen! Sometimes it works out the previous option faster! How is that??
Yandex, what a joke? So much time to spend to try it all, and to get such a result - well, it's generally sad.

I thought, thought ... Well, not really the same. Give a person a controller with cards and put him into a stupor when switching to it for more than 4 seconds - this is suicide for the application. No one will wait more than 5 seconds with full confidence that this is comfortable (if you do not believe me, listen to Vitaliy Fridman's reports on UI / UX).

I thought more ... and the following emotion was this:


Who wants with sound - www.youtube.com/watch?v=pTZaNHZGsQo

The recipe for success was this:
Take a kilo ... CLLocationManager and YMKLocationManager and ... make them work together.



This joint ... "work" looks something like this:

 // 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) } } 


... and the pilaf is ready

The result of the speed of getting the user's point: a little more than 0 seconds.

The very case when (in my opinion) when solving a trivial problem, the opposition of the native and embedded parts looked like this:



Geocoder *
As a bonus, I’ll show the implementation of the geocoder.
Used Yandex Geocoder 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 } } 

This resource helps a lot to convert the response from the server to the model: app.quicktype.io

The result of the work visually is as follows:



Summarizing the above:
colleagues, the article is written so that when solving such a problem you do not spend as much time as I spent and either choose a different path or walk it quickly.

I would like to see constructive criticism and / or alternative correct solutions.

If the article turned out to be useful to you, correct me with a Lois rating and, preferably, check out the original version of this story in a new way.

All creative success and positive mood!

Source: https://habr.com/ru/post/479432/


All Articles