Introducing Yandex Maps MapKit to your iOS application

A quick guide for non-Russian speakers

Sergey Petryaev
9 min readJul 7, 2023
Cover Image with Yandex MapKit logo and an iPad

Let us say, that you are an iOS developer and somehow you just got assigned to a task to add Yandex Maps to your app — or you’re simply interested in learning something new regarding Yandex Tech — It’s unlikely you can’t read articles or watch guides in Russian. But if it’s not the case or it’s just a personal preference to read in English, then this article is for you.

This article provides a step-by-step guide on how to:

  • add a placemark on the map with coordinates or address;
  • select a placemark and retrieve its data;
  • add a large amount of placemarks as a cluster;
  • and more.

Requirements

This article provides examples of user interface built with UIKit (programmatically)

To successfully add Yandex MapKit into your iOS project, you will need:

  • MacOS Catalina or newer;
  • Xcode 12.1 or newer;
  • Homebrew;
  • Ruby;
  • CocoaPods;
  • API key;
  • Target iOS 12 or newer (iOS 13+ if you’re on Apple Silicon).

Please note that requirements may be changed in the future.

A quick note for Apple Silicon users

If you are using an Apple Silicon based machine, you may experience unexpected crashes in Simulator. This is relevant for MapKit SDK 4.2.2 and older.

Solution: upgrade to MapKit SDK 4.3.0.

Demo Application

There’s a Demo App developed by Yandex Maps Team that you may find helpful. It provides simple examples of casual tasks a developer can do. Although the repository lacks detailed step-by-step explanations.

A complication of screenshots showing what the MapKit Demo app is capable of
A complication of screenshots showing what the MapKit Demo app is capable of.

Getting started

Adding the SDK to your project

At the moment, there is only one way to add the dependency — CocoaPods package manager. If you don’t have it set up, please refer to the official docs.

pod 'YandexMapsMobile', '4.3.1-full'

Please note that there are two versions of the dependency: lite and full. If your goal is to just show the map and put a pin to it, you can’t go wrong with the lite. However, the full can do lots of things above that: navigation, public transit, search, suggestions, geocoding, panoramas and more.

After you’re done with configuring dependencies, you can go ahead and import YandexMapsMobile. Here’s an example of how it would look like in your 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
}

}

Let’s create a base UIView for future use:

Initializing the map and adding a placemark

To get the map displayed, create a new view controller and add the base view as subview:

final class YandexMapSampleViewController: UIViewController {
// 1. Create the base view var
lazy var mapView: YMKMapView = YBaseMapView().mapView


override func viewDidLoad() {
super.viewDidLoad()
// 2. Add to parent in viewDidLoad()
view.addSubview(mapView)

// 3. Make constraints. Showing an example done with SnapKit
mapView.snp.makeConstraints {
$0.leading.trailing.top.equalToSuperview()
$0.bottom.equalTo(view.safeAreaLayoutGuide)
}

// 4. Call another function to add placemark on the map
self.addPlacemarkOnMap()
}

}

And here’s a custom function to add placemark on the map as reference:

func addPlacemarkOnMap() {
// Setting coordinates
let point = YMKPoint(latitude: 47.228836, longitude: 39.715875)
let viewPlacemark: YMKPlacemarkMapObject = mapView.mapWindow.map.mapObjects.addPlacemark(with: point)

// Configuring placemark icon
viewPlacemark.setIconWith(
UIImage(named: "map_search_result_primary")!, // Be sure to check that you have this asset
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
)
)
}

The result:

Result of adding a placemark on the map.

Making placemarks interactive

In order to make a placemark interactive and track tap gestures, you will need to implement YMKMapObjectTapListener interface and bind it to the placemark as an event listener. You may already know it, but we can make use of Swift’s class extensions so our main class doesn’t look like a mess:

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

Sample:

extension YandexMapSampleViewController: YMKMapObjectTapListener {
func onMapObjectTap(with mapObject: YMKMapObject, point: YMKPoint) -> Bool {
guard let placemark = mapObject as? YMKPlacemarkMapObject else {
// Error
return false
}
// Success
self.focusOnPlacemark(placemark)
return true
}

func focusOnPlacemark(_ placemark: YMKPlacemarkMapObject) {
// Move the camera to focus on the placemark
mapView.mapWindow.map.move(
with: YMKCameraPosition(target: placemark.geometry, zoom: 18, azimuth: 0, tilt: 0),
animationType: YMKAnimation(type: YMKAnimationType.smooth, duration: duration),
cameraCallback: nil
)
}
}

And here’s how you bind your tap listener to a placemark:

// Here is supposed to be code that creates the placemark variable
viewPlacemark.addTapListener(with: self)

After doing this, any tap gesture will fire the function you implemented in the event listener.

Adding user data to placemark

So you want to tap on a placemark and have the map zoomed in and address displayed on the screen. We just solved the first part of it — the tap listener does exactly that now. To tackle the second part, you will need to populate the YMKPlacemarkMapObject.userData variable. You can put any address or title as a string, or any other object depending on your needs.

let viewPlacemark: YMKPlacemarkMapObject = mapView.mapWindow.map.mapObjects.addPlacemark(with: point)
/* Here's supposed to be code, adding an image to the placepark or whatever */
viewPlacemark.userData = "Placemark Title"

And then, when it comes to using the data you are storing as placemark user data, you just access it as a variable and cast if needed. Here’s an example:

func focusOnPlacemark(placemark: YMKPlacemarkMapObject) {
// Move camera to focus on placemark
mapView.mapWindow.map.move(
with: YMKCameraPosition(target: placemark.geometry, zoom: 18, azimuth: 0, tilt: 0),
animationType: YMKAnimation(type: YMKAnimationType.smooth, duration: duration),
cameraCallback: nil // Optional callback on when the camera stops moving
)

if let placemarkName: String = placemark.userData as? String {
// Example
self.displaySelectedPlacemarkName(placemarkName)
} else {
// do nothing
}
}

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

The result:

Screenshots of the map demonstrating interactive features of the placemark on tap.
Demonstration of interactive features of the placemark on tap.

Address search

Before getting to the search, make sure you’re using the full version of the SDK.

To put a placemark on a map, you need coordinates, right? Now, if only have an address, there must be a way to convert it to coordinates — and there is! It’s called [direct] geocoding (or the search). FYI reverse geocoding does exactly the opposite — it gives you address assigned to coordinates you provide.

Below is a sample code snippet of a simple search service you can use:

And an example of how to use it:

func showAddressOnMap(_ address: String) {
interactor?.searchAddress(address) { [weak self] response in
// In this example, we're processing only the first search result
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 more information on this topic, I would suggest exploring the official developer docs, such as YMKSearchManager class summary. Pay attention to the geometry and searchOptions parameters – it gives you more flexibility in terms of search capabilities.

For instance, the geometry param’s flexibility can get you different search bevahiors:

  • A point prioritizes the search to run within a small rectangular area nearby the point;
  • A bounding box or a polygon is simply a window to prioritize the search within. It’s highly helpful if you need to use the device’s screen to limit the number of results rendering at the same time;
  • A polyline prioritizes the search to run within an area the polyline covers as if it was fit into a rectangular window.

Processing a large amount of placemarks

When it comes to rendering lots of placemarks, you just add them to the map. However, it may be unclear how it would look like if a single place is congested with placemarks. Also, wouldn’t it be excessive to run hundreds of search requests from a mobile device? This might take a serious hit on performance and license limitations.

Clusterization

It’s recommended to leverage placemark clusterization to clear the map from visual clutter. Here’s a quick comparison you can refer to:

Restult with clusterization off (top) and on (bottom)

Implementing this is not that big of a deal. In fact, once you configure it, it’s pretty similar to how you would put a single placemark on the map. The difference is that instead of mapView.mapWindow.map.mapObjects.addPlacemark() you should use clusteredColletion.addPlacemark(). The latter is a collection created via mapView.mapWindow.map.mapObjects.addClusterizedPlacemarkCollection().

After adding placemarks into a cluster (or a set of clusters), you need to call clusteredColletion?.clusterPlacemarks() to render the clustered placemarks on the map properly.

Keep in mind that you are empowered to customize what the cluster looks like. To say, you will probably want to display a number of placemarks that are currently grouped into a cluster — and you can! There’s a function you can implement to dynamically change cluster image depending of it’s current size. This is how you do it:

And this is how you set up a camera zoom action when tapping 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
}
}

My personal recommendation is to implement YMKMapCameraListener and its onCameraPositionChanged function, store cameraPosition.zoom somewhere and calculate the zoom on such actions like tap to zoom. That way, you can avoid sudden movements of the camera and make the UX more fluid.

That’s it! Now you have a visually-pleasing map with hundreds of interactive placemarks!

Multi Geocoding

To add a placemark on the map, you’ve got to have coordinates and if you don’t, you run the search by address. Although It works out pretty well for single placemarks, running lots of geocoder requests on a mobile device is insufficient in terms of performance and license limitations. In this case a good solution would be doing multi geocoding on the server-side.

The approach is quite simple: the server retrieves a list of data as a request from your mobile device, fetches coordinates from the HTTP Geocoder API one-by-one and returns it back to the device.

At the moment I put this article together, there is only single time Yandex mentioned this in their official docs around year 2014 and it hasn’t been updated since. There is a Node.js dependency that can do the server-side processing. If your stack is not based on node — you will have to implement it manually.

Note that you will have to have an HTTP Geocoder API key beforehand. A few links regarding this approach:

Below is a sample of how you would use the Node.js dependency. The example from GitHub didn’t quite work because of the misspelled apikey param – it said key which is incorrect.

In addition, I found two implementations of this approach you may find helpful. Those are made by enthusiasts, so be careful if you are thinking of incorporating it into a production app.

in C#:

https://github.com/unickq/YandexGeocoder

and PHP:

Quick note on Terms of Service

You can use Yandex Maps API for free (with a moderate API usage limit in place), but you are not allowed to store or modify any retrieved data. It means when you’re searching coordinates by address, storing those coordinates is prohibited.

The commercial license can lift this restriction, but only if you are willing to pay for the extended edition. The standard commercial license is still limited in terms of allowed data manipulations.

https://yandex.ru/dev/maps/commercial/doc/concepts/about-enterprise.html

But Yandex still let us to cache the data for up to 30 days. Nevertheless, my advice is to contact customer support and discuss your specific case directly.

So this was a condensed summary of how to get started with Yandex Maps MapKit for iOS. Yandex recently updated their docs and although you can now find class descriptions in Swift, it doesn’t provide any detailed guides. Hence, I hope you find my own experience condensed into this article helpful.

Disclaimer: this is an adaptation of my own article in Russian. Here’s a link to the original if you’re interested:

--

--