Implementing MapKit Yandex Maps in an iOS application

ed. Cherepovy Daria

Hi all! My name is Serezha and I have been doing web and iOS development for 5 years now. On one of the projects, I was given the task of implementing Yandex Maps in the application. However, I am faced with the fact that there is little necessary and useful information about this topic in the public domain. This article is my way of solving problems in Swift using Yandex MapKit. I share with you my experience!

In this article, I talk about how:

  • Show a point on the map by coordinates or address;

  • Select a specific point and display its data;

  • Display a large number of points by address.

Preparation

💡 To create the interface, I used the programmatic method (UIKit)

To work with Yandex MapKit on iOS, you need:

  • macOS Catalina and above

  • Xcode (preferably 12.1 and above)

  • homebrew

  • ruby

  • CocoaPods

  • The key to access the Yandex API (can be requested free of charge)

  • Target iOS 12 and above (iOS 13+ for Apple Silicon)

    Please note that requirements may change. The article was written in June 2023.

Working on Apple Silicon

When running on Apple Silicon processors, on versions of MapKit SDK below 4.3.0, the application may crash when opening a map. Especially during debugging. Error example:

An example of an error on a MacBook with an M1 processor

An example of an error on a MacBook with an M1 processor

With MapKit SDK version 4.3.0 (March 2, 2023), the following update has been added:

For emulators with an M1 processor, the card will automatically switch to use metal API.
Source: Versions of MapKit – Yandex MapKit. Developer Guide

After updating to the latest version, the crashes stopped for no reason. There were no problems on the real device either.

Yandex Demo Application

The MapKit demo app allows you to use the features of Yandex.Maps in mobile apps for iOS and Android. There are sections here:

  • Map display;

  • Adding objects and pins;

  • User location;

  • Traffic jams;

  • Panorama;

  • Search and hints;

  • Selecting an object on click.

Screenshots of Yandex MapKit Demo Application

Screenshots Yandex MapKit Demo Application

Just in case, I’ll leave links to the MapKit manual from the developer and the demo version of the application:

How to get started with MapKit for iOS – Yandex MapKit. Developer Guide

Yandex MapKit Demo Application – GitHub

Working with the map

Adding maps to a project

First you need to add a dependency to CocoaPods in order to load the library.

💡 If CocoaPods is not connected in the project, then you need to initialize it. To do this, in the terminal in the root directory, write pod init.

After initialization, the project must be opened using a new project file with the extension .xcworkspace (example: MapKitDemo.xcworkspace)

pod 'YandexMapsMobile', '4.3.1-full’

It should be noted that there are two versions of the library: lite and full. Lite version allows you to work with online and offline maps, shows location and traffic jams. Full version provides routes for cars, bicycles, pedestrians, public transport; search; hints; geocoding; panorama display.

After adding dependencies, it becomes possible to do import YandexMapsMobile . Add the following lines to the main method AppDelegate:

/* imports */
import YandexMapsMobile

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

	func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
			/* code */
	    
	    /* Init YandexMaps MapKit */
	    YMKMapKit.setApiKey("your-api-key")
	    YMKMapKit.setLocale("ru_RU")
	    YMKMapKit.sharedInstance()
	    
			/* code */
	    return true
	} 

}

Create a basic UIView for ease of use in other modules:

import UIKit
import YandexMapsMobile

class YBaseMapView: UIView {

    @objc public var mapView: YMKMapView!

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }

    private func setup() {
        // OpenGl is deprecated under M1 simulator, we should use Vulkan
        mapView = YMKMapView(frame: bounds, vulkanPreferred: YBaseMapView.isM1Simulator())
        mapView.mapWindow.map.mapType = .map
    }

    static func isM1Simulator() -> Bool {
        return (TARGET_IPHONE_SIMULATOR & TARGET_CPU_ARM64) != 0
    }
}

Ready!

Create a map and add a point

Add a pre-created view into a new component to display the map:

final class YandexMapSampleViewController: UIViewController {
	// 1. Создать элемент
	lazy var mapView: YMKMapView = YBaseMapView().mapView
	
	
	override func viewDidLoad() {
		super.viewDidLoad()
	  // 2. Добавить в родительский view во viewDidLoad()
		view.addSubview(mapView)
	
		// 3. Настроить constraints. Приведён пример со SnapKit
		mapView.snp.makeConstraints {
		    $0.leading.trailing.top.equalToSuperview()
		    $0.bottom.equalTo(view.safeAreaLayoutGuide)
		}
	
	  // 4. Вызов функции добавления точки на карту
		self.addPlacemarkOnMap()
	}

}

Now we create a separate function for adding a point to the map:

func addPlacemarkOnMap() {
   // Задание координат точки
	let point = YMKPoint(latitude: 47.228836, longitude: 39.715875)
	let viewPlacemark: YMKPlacemarkMapObject = mapView.mapWindow.map.mapObjects.addPlacemark(with: point)
	
  // Настройка и добавление иконки
	viewPlacemark.setIconWith(
	    UIImage(named: "map_search_result_primary")!, // Убедитесь, что у вас есть иконка для точки
	    style: YMKIconStyle(
	        anchor: CGPoint(x: 0.5, y: 0.5) as NSValue,
	        rotationType: YMKRotationType.rotate.rawValue as NSNumber,
	        zIndex: 0,
	        flat: true,
	        visible: true,
	        scale: 1.5,
	        tappableArea: nil
	    )
	)
}

Result:

Adding a point to the map

Adding a point to the map

Interactive points on the map

To make a point clickable, you need to implement the interface YMKMapObjectTapListener and specify it when creating a point. I’m using the Swift class extension which makes the code easier to understand and doesn’t clutter up the main class.

In the event listener implementation, we are prompted to create an implementation for one function:

extension YandexMapSampleViewController: YMKMapObjectTapListener {
    func onMapObjectTap(with mapObject: YMKMapObject, point: YMKPoint) -> Bool {
        // your code here
    }
}

Example:

extension YandexMapSampleViewController: YMKMapObjectTapListener {
    func onMapObjectTap(with mapObject: YMKMapObject, point: YMKPoint) -> Bool {
        guard let placemark = mapObject as? YMKPlacemarkMapObject else {
            // Сценарий на случай ошибки
            return false
        }
        // Сценарий на случай успеха. Бизнес-логику добавляют сюда.
				// Пример
				self.focusOnPlacemark(placemark)
        return true
    }

		func focusOnPlacemark(_ placemark: YMKPlacemarkMapObject) {
			// Поменять расположение камеры, чтобы сфокусироваться на точке
			mapView.mapWindow.map.move(
            with: YMKCameraPosition(target: placemark.geometry, zoom: 18, azimuth: 0, tilt: 0),
            animationType: YMKAnimation(type: YMKAnimationType.smooth, duration: duration),
            cameraCallback: nil
      )
		}
}

For the event listener to work, you need to specify it when creating the point:

// Создание переменной точки не показано (см. пример выше)
viewPlacemark.addTapListener(with: self)

After that, any click on the point will follow through the created function.

Point User Data

Task: zoom in on the map and display the data on the screen by clicking on a point on the map. Solution: when creating each point, you need to fill in a variable YMKPlacemarkMapObject.userData the necessary data. It can be a string with a name/address, or another object of any type.

let viewPlacemark: YMKPlacemarkMapObject = mapView.mapWindow.map.mapObjects.addPlacemark(with: point)
/* Здесь будет код, добавляющий иконку к точке */
viewPlacemark.userData = "Точка на карте" 

To use the passed points, you need to access them and, if necessary, cast them to the desired data type.

An example of the function of focusing the camera on a point:

func focusOnPlacemark(placemark: YMKPlacemarkMapObject) {
	// Поменять расположение камеры, чтобы сфокусироваться на точке
	mapView.mapWindow.map.move(
	      with: YMKCameraPosition(target: placemark.geometry, zoom: 18, azimuth: 0, tilt: 0),
	      animationType: YMKAnimation(type: YMKAnimationType.smooth, duration: duration),
	      cameraCallback: nil // Опциональный callback по завершению работы камеры
	)

	if let placemarkName: String = placemark.userData as? String {
		// Пример
		self.displaySelectedPlacemarkName(placemarkName)
	} else {
		// do nothing
	}
}

func displaySelectedPlacemarkName(_ placemarkName: String) {
	// your code here
}

An example of a pre-built panel for displaying point data:

An example of an action when clicking on a dot

An example of an action when clicking on a dot

Search by address

To search by address (direct geocoding), use full MapKit version (lite won’t fit).

We do this: we send the address and receive in response the coordinates that can be shown on the map.

Example class with search:

final class YandexMapsAddressSearchInteractor {
	lazy var searchManager: YMKSearchManager? = YMKSearch.sharedInstance().createSearchManager(with: .combined)
  var searchSession: YMKSearchSession?
  
	// Окно поиска
  let BOUNDING_BOX = YMKBoundingBox(
      southWest: YMKPoint(latitude: 55.55, longitude: 37.42),
      northEast: YMKPoint(latitude: 55.95, longitude: 37.82)
  )


	func searchAddress(_ address: String?, completion: @escaping(YMapsSearchVoid)) {
	  guard let address = address else {
	      return
	  }
	
	  searchManager = YMKSearch.sharedInstance().createSearchManager(with: .combined)
	  
		// Callback функция, которая выполняется по завершению поиска
	  let responseHandler = { (searchResponse: YMKSearchResponse?, error: Error?) -> Void in
	      if let response = searchResponse {
						// Передаваемая callback функция. Обрабатывать результат нужно здесь
	          completion(response)
	      } else {
	          let searchError = (error! as NSError).userInfo[YRTUnderlyingErrorKey] as! YRTError
	          var errorMessage = L10n.ymapsUnknownError
	          if searchError.isKind(of: YRTNetworkError.self) || searchError.isKind(of: YMKSearchCacheUnavailableError.self) {
	              errorMessage = L10n.ymapsNetworkError
	          } else if searchError.isKind(of: YRTRemoteError.self) {
	              errorMessage = L10n.ymapsServerError
	          }
	          // showErrorMessage(errorMessage)
	      }
	  }
	  
	  searchSession = searchManager!.submit(
	      withText: address,
	      geometry: YMKGeometry(boundingBox: BOUNDING_BOX),
	      searchOptions: YMKSearchOptions(),
	      responseHandler: responseHandler
	  )
	}
}

Search usage example:

func showAddressOnMap(_ address: String) {
	interactor?.searchAddress(address) { [weak self] response in
		// Обработка только первого результата из ответа
    if response.collection.children.count > 0 {
        let searchResults: [YMKGeoObjectCollectionItem] = response.collection.children

        if let mapObject = searchResults[0].obj {
            if let point = mapObject.geometry.first?.point {
                self?.view?.addPlacemarkOnMap(point)
            }
        }
    } else {
		  // self?.showSearchError("No results found")
    }
	}
}

For a more detailed study of Yandex Maps search, I advise you to read this article. It describes the work of the search and its modules with examples. Note that most of the examples are for Android, but they also apply to iOS.

Here is how the parameter is described geometry In this article:

Parameter geometry a little more cunning. Depending on which geometry is passed, the search will behave differently:

If you pass a point, then the search will be performed in a small window next to this point. If we pass a rectangular window (BoundingBox) or polygon of four dots, it will be used as the search box. A simple example of such a window is the visible area of ​​the map. Finally, if we pass polylinethen the window describing it will be used as a search box, and the ranking will be made taking into account this polyline.

Working with multiple points

If you need to show many points at once, add several hundred (or thousand) points to the map, following the example described above. But what would a large number of points look like on a map? And what if there is only a list of addresses?

Clustering

In order not to visually load the map, we apply clustering of points.

top - without clustering;  bottom - with clustering

top – without clustering; bottom – with clustering

The implementation is similar to the usual adding a point to the map. The difference is that instead of mapView.mapWindow.map.mapObjects.addPlacemark() used clusteredColletion.addPlacemark()created with mapView.mapWindow.map.mapObjects.addClusterizedPlacemarkCollection(). After adding points to the cluster(s), we call the function clusteredColletion?.clusterPlacemarks() for the correct display of clusters on the map.

final class YandexMapClusterSampleViewController: UIViewController {
	// Переменная коллекции-кластера
	private var clusteredColletion: YMKClusterizedPlacemarkCollection? = nil
	
	func viewDidLoad() {
		super.viewDidLoad()
		// Инициализация коллекции-кластера
		clusteredColletion = mapView.mapWindow.map.mapObjects.addClusterizedPlacemarkCollection(with: self)	
		renderClusters()
	}

	// Фукнция для добавлении точки в кластер
	func addPlacemarkToCluster(point: YMKPoint) {
		guard let clusteredColletion = clusteredColletion else {
        return
    }
		// Добавление точки в кластер
		let viewPlacemark: YMKPlacemarkMapObject = clusteredColletion.addPlacemark(with: point)
		
	  // Настройка и добавление иконки
		viewPlacemark.setIconWith(
		    UIImage(named: "map_search_result_primary")!, // Убедитесь, что у вас есть иконка для точки
		    style: YMKIconStyle(
		        anchor: CGPoint(x: 0.5, y: 0.5) as NSValue,
		        rotationType: YMKRotationType.rotate.rawValue as NSNumber,
		        zIndex: 0,
		        flat: true,
		        visible: true,
		        scale: 1.5,
		        tappableArea: nil
		    )
		)
	}
	
		// Фукнция для отображения кластера
	private func renderClusters() {
	  self.clusteredColletion?.clusterPlacemarks(withClusterRadius: 60, minZoom: UInt(OutletMapView.DEFAULT_CAMERA_ZOOM))
  }

}

To customize the visual part (for example, as in the screenshot), you need to implement a function that dynamically creates a new cluster image depending on the number of points grouped in it. This is done in the following way:

extension YandexMapClusterSampleViewController: YMKClusterListener {
		func onClusterAdded(with cluster: YMKCluster) {
        cluster.appearance.setIconWith(clusterImage(cluster.size))
        cluster.addClusterTapListener(with: self)
    }    

    func clusterImage(_ clusterSize: UInt) -> UIImage {
        let scale = UIScreen.main.scale
        let text = (clusterSize as NSNumber).stringValue
        let font = UIFont.systemFont(ofSize: FONT_SIZE * scale)
        let size = text.size(withAttributes: [NSAttributedString.Key.font: font])
        let textRadius = sqrt(size.height * size.height + size.width * size.width) / 2
        let internalRadius = textRadius + MARGIN_SIZE * scale
        let externalRadius = internalRadius + STROKE_SIZE * scale
        let iconSize = CGSize(width: externalRadius * 2, height: externalRadius * 2)

        UIGraphicsBeginImageContext(iconSize)
        let ctx = UIGraphicsGetCurrentContext()!

        ctx.setFillColor(Asset.green.color.cgColor)
        ctx.fillEllipse(in: CGRect(
            origin: .zero,
            size: CGSize(width: 2 * externalRadius, height: 2 * externalRadius)));

        ctx.setFillColor(UIColor.white.cgColor)
        ctx.fillEllipse(in: CGRect(
            origin: CGPoint(x: externalRadius - internalRadius, y: externalRadius - internalRadius),
            size: CGSize(width: 2 * internalRadius, height: 2 * internalRadius)));

        (text as NSString).draw(
            in: CGRect(
                origin: CGPoint(x: externalRadius - size.width / 2, y: externalRadius - size.height / 2),
                size: size),
            withAttributes: [
                NSAttributedString.Key.font: font,
                NSAttributedString.Key.foregroundColor: UIColor.black])
        let image = UIGraphicsGetImageFromCurrentImageContext()!
        return image
    }
}

An example of code that zooms in on the camera when clicking on a cluster:

extension YandexMapClusterSampleViewController: YMKClusterTapListener {
	func onClusterTap(with cluster: YMKCluster) -> Bool {
        mapView.mapWindow.map.move(
            with: YMKCameraPosition(target: cluster.appearance.geometry, zoom: 20, azimuth: 0, tilt: 0),
            animationType: YMKAnimation(type: YMKAnimationType.smooth, duration: 0,4),
            cameraCallback: completion
        )
        return true
  }
}

💡 I recommend that you additionally implement the YMKMapCameraListener interface with its onCameraPositionChanged method and store the cameraPosition.zoom value so that you can calculate the required camera zoom and change it without sudden jumps.

You are super! Now the map is not visually overloaded with many points!

Multiple geocoding

If the points do not have pre-stored coordinates, you need to look for them by address. In the case of working with one or two points, requesting the Yandex API from the client is quite common, but when it comes to hundreds – or even thousands of points – a reasonable solution would be to use multiple geocoding on the server side. Performing thousands of requests from a mobile device is unprofitable in terms of performance and request limit according to the license.

Multiple geocoding works like this: the server receives a list of addresses, and for each one makes a request to the Yandex HTTP Geocoder.

At the time of this writing, Yandex has published only one fast on this topic (back in 2014). Unfortunately, in its documentation, Yandex includes only one example and a library written for Node.js. To use multiple geocoding in applications in other programming languages, you will have to implement calls to the API manually.

Map search – Multiple geocoding. Yandex Documentation

node-multi-geocoder – npm

node-multi-geocoder – GitHub

💡 HTTP Geocoder requires a developer key to access the JavaScript API and Geocoder.

Below is the code using the library node-multi-geocoder.

Example from GitHub didn’t work due to parameter apikeywhich has been described as key. I checked.

let geocoder = new MultiGeocoder({ provider: 'yandex', coordorder: 'latlong'});
let provider = geocoder.getProvider();

let getRequestParams = provider.getRequestParams;
provider.getRequestParams = function() {
    let result = getRequestParams.apply(provider, arguments);
    result.apikey = "your-key-here";
    return result;
}

let geoResponse = await geocoder.geocode([
    "address 1",
    "address 2",
    "address 3",
    "address 4"
]);
Library analogues

I found a repository with similar functionality written in C#:

YandexGeocoder – GitHub

And php:

yandex-geocoder – GitHub

The nuances of the user agreement

You can use the Yandex Maps API for free, but in this case it is forbidden to save the result of the geocoder. In other words, if you search for coordinates by address, then it is forbidden to save the coordinates to your database.

A commercial license allows you to remove this restriction:

Commercial version of the Yandex.Maps API – Yandex Documentation

💡 Commercial license is divided into standard (from 120 thousand rubles per year) and extended (from 620 thousand rubles per year). In the standard license forbidden store or modify the data received using the API. Source

As a compromise, Yandex suggests caching the result for a maximum of 30 days. I advise you to contact Yandex support with your specific case and clarify legal issues.

Conclusion

MapKit SDK Yandex Maps for iOS opens up useful features that are important for iOS developers to be able to use in Russia. The Yandex documentation has recently been updated, and now you can find a description of classes and methods in Swift, but there are not so many clear examples. In my article, I revealed the main approaches to working with MapKit from Yandex and shared my personal experience. Hope the article helps you!

useful links

How to get started with MapKit for iOS – Yandex MapKit. Developer Guide

Yandex MapKit Demo Application – GitHub

A story about how I updated Yandex MapKit on iOS or maps, money, 2 map kits

Yandex.Maps: I went to the map controller and immediately got the user’s position (ok, now seriously)

MapKit Search: Tips & Tricks

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *