iOS SDK Examples (unstable) | 2GIS Documentation
iOS SDK

To begin working with the SDK, create a Container object, which will store all map entities. To create it, you need to specify your API keys as an APIKeys structure.

// Create an APIKeys object.
guard let apiKeys = APIKeys(directory: "Directory API key", map: "SDK key") else { 
	fatalError("Invalid API keys.") 
}

// Create a Container object.
let sdk = DGis.Container(apiKeys: apiKeys)

Note that DGis.Container can be created in single instance.

Additionally, you can specify logging settings (LogOptions) and HTTP client settings (HTTPOptions) such as timeout and caching.

// Logging settings.
let logOptions = LogOptions(osLogLevel: .info)

// HTTP client settings.
let httpOptions = HTTPOptions(timeout: 5, cacheOptions: nil)

// Geopositioning settings.
let positioningServices: IPositioningServicesFactory = CustomPositioningServicesFactory()

// Consent to personal data processing.
let dataCollectionOptions = DataCollectionOptions(dataCollectionStatus: .agree)

// Creating the Container.
let sdk = DGis.Container(
	apiKeys: apiKeys,
	logOptions: logOptions,
	httpOptions: httpOptions,
	positioningServices: positioningServices,
	dataCollectionOptions: dataCollectionOptions
)

First you need to contact 2GIS technical support to get a new key. Be sure to specify the appId of the application for which the key will be generated.

To begin working with the SDK, create a Container object, which will store all map entities.

To create it, you need to specify path to your key file via ApiKeyOptions. When specifying ApiKeyOptions.default, the file must be added to the application root.

// Key file.
let apiKeyOptions = ApiKeyOptions(apiKeyFile: File(path: "Path to key info file"))

// Создание контейнера для доступа к возможностям SDK.
let sdk = DGis.Container(apiKeyOptions: apiKeyOptions)

Note that DGis.Container can be created in single instance.

Additionally, you can specify logging settings (LogOptions) and HTTP client settings (HTTPOptions) such as timeout and caching.

// Logging settings.
let logOptions = LogOptions(osLogLevel: .info)

// HTTP client settings.
let httpOptions = HTTPOptions(timeout: 5, cacheOptions: nil)

// Geopositioning settings.
let positioningServices: IPositioningServicesFactory = CustomPositioningServicesFactory()

// Consent to personal data processing.
let dataCollectionOptions = DataCollectionOptions(dataCollectionStatus: .agree)

// Creating the Container.
let sdk = DGis.Container(
	apiKeyOptions: apiKeyOptions,
	logOptions: logOptions,
	httpOptions: httpOptions,
	positioningServices: positioningServices,
	dataCollectionOptions: dataCollectionOptions
)

To create a map, call the makeMapFactory() method and specify the required map settings as a MapOptions structure.

It is important to specify the correct PPI settings for the device. You can find them in the technical specification of the device.

You can also specify the initial camera position, zoom limits, and other settings.

// Map settings object.
var mapOptions = MapOptions.default

// PPI settings.
mapOptions.devicePPI = devicePPI

// Create a map.
let mapFactory: PlatformMapSDK.IMapFactory = sdk.makeMapFactory(options: mapOptions)

To get the view of the map, use the mapView property. To get the control of the map, use the map property.

// Map view.
let mapView: UIView & IMapView = mapFactory.mapView

// Map control.
let map = mapFactory.map

Some SDK methods (e.g., those that access a remote server) return deferred results (Future). To process a deferred result, you can specify two callback functions: completion and error. To move the execution to the main thread, you can use DispatchQueue.

For example, to get information from object directory, you can process Future like so:

// Create an object for directory search.
let searchManager = SearchManager.createOnlineManager(context: sdk.context)

// Get object by identifier.
let future = searchManager.searchByDirectoryObjectId(objectId: object.id)

// Process the search result in the main thread.
// Save the result to a property to prevent garbage collection.
self.searchDirectoryObjectCancellable = future.sink(
	receiveValue: {
		[weak self] directoryObject in
		guard let directoryObject = directoryObject else { return }
		DispatchQueue.main.async {
			self.handle(directoryObject)
		}
	},
	failure: { error in
		DispatchQueue.main.async {
			self.handle(error)
		}
	}
)

To simplify working with deferred results, you can create an extension:

extension DGis.Future {
	func sinkOnMainThread(
		receiveValue: @escaping (Value) -> Void,
		failure: @escaping (Error) -> Void
	) -> DGis.Cancellable {
		self.sink(on: .main, receiveValue: receiveValue, failure: failure)
	}

	func sink(
		on queue: DispatchQueue,
		receiveValue: @escaping (Value) -> Void,
		failure: @escaping (Error) -> Void
	) -> DGis.Cancellable {
		self.sink { value in
			queue.async {
				receiveValue(value)
			}
		} failure: { error in
			queue.async {
				failure(error)
			}
		}
	}
}

self.searchDirectoryObjectCancellable = future.sinkOnMainThread(
	receiveValue: {
		[weak self] directoryObject in
		guard let directoryObject = directoryObject else { return }
		self.handle(directoryObject)
	},
	failure: { error in
		self.handle(error)
	}
)

Or use the Combine framework:

// Extension to convert DGis.Future to Combine.Future
extension DGis.Future {
	func asCombineFuture() -> Combine.Future<Value, Error> {
		Combine.Future { [self] promise in
			// Save the Cancellable object until the callback function is called.
			// Combine does not support cancelling Future directly.
			var cancellable: DGis.Cancellable?
			cancellable = self.sink {
				promise(.success($0))
				_ = cancellable
			} failure: {
				promise(.failure($0))
				_ = cancellable
			}
		}
	}
}

// Create Combine.Future
let combineFuture = future.asCombineFuture()

// Process the search result in the main thread.
combineFuture.receive(on: DispatchQueue.main).sink {
	[weak self] completion in
	switch completion {
		case .failure(let error):
			self?.handle(error)
		case .finished:
			break
	}
} receiveValue: {
	[weak self] directoryObject in
	self?.handle(directoryObject)
}.store(in: &self.subscriptions)

Some SDK objects provide data channels (see the Channel class). To subscribe to a data channel, you need to create and specify a handler function.

For example, you can subscribe to a visible rectangle channel, which is updated when the visible area of the map is changed:

// Choose a data channel.
let visibleRectChannel = map.camera.visibleRectChannel

// Subscribe to the channel and process the results in the main thread.
// It is important to prevent the connection object from getting garbage collected to keep the subscription active.
self.cancellable = visibleRectChannel.sink { [weak self] visibleRect in
	DispatchQueue.main.async {
		self?.handle(visibleRect)
	}
}

When the data processing is no longer required, it is important to close the connection to avoid memory leaks. To do this, call the cancel() method:

self.cancellable.cancel()

You can create an extension to simplify working with data channels:

extension Channel {
	func sinkOnMainThread(receiveValue: @escaping (Value) -> Void) -> DGis.Cancellable {
		self.sink(on: .main, receiveValue: receiveValue)
	}

	func sink(on queue: DispatchQueue, receiveValue: @escaping (Value) -> Void) -> DGis.Cancellable {
		self.sink { value in
			queue.async {
				receiveValue(value)
			}
		}
	}
}

self.cancellable = visibleRectChannel.sinkOnMainThread { [weak self] visibleRect in
	self?.handle(visibleRect)
}

To add dynamic objects to the map (such as markers, lines, circles, and polygons), you must first create a MapObjectManager object, specifying the map instance. Deleting an object manager removes all associated objects from the map, so do not forget to save it to a property.

self.objectsManager = MapObjectManager(map: map)

After you have created an object manager, you can add objects to the map using the addObject() and addObjects() methods. For each dynamic object, you can specify a userData field to store arbitrary data. Object settings can be changed after their creation.

To remove objects from the map, use removeObject() and removeObjects(). To remove all objects, call the removeAll() method.

To add a marker to the map, create a Marker object, specifying the required options (MarkerOptions), and pass it to the addObject() method of the object manager. The most important settings are the coordinates of the marker and its icon.

You can create an icon for the marker by calling the make() method and using UIImage, PNG data, or SVG markup as input.

// UIImage
let uiImage = UIImage(systemName: "umbrella.fill")!.withTintColor(.systemRed)
let icon = sdk.imageFactory.make(image: uiImage)

// SVG markup.
let icon = sdk.imageFactory.make(svgData: imageData, size: imageSize)

// PNG data.
let icon = sdk.imageFactory.make(pngData: imageData, size: imageSize)

// Marker settings.
let options = MarkerOptions(
	position: GeoPointWithElevation(
		latitude: 55.752425,
		longitude: 37.613983
	),
	icon: icon
)

// Create and add the marker to the map.
let marker = Marker(options: options)
objectManager.addObject(object: marker)

To change the hotspot of the icon, use the anchor parameter.

To draw a line on the map, create a Polyline object, specifying the required options, and pass it to the addObject() method of the object manager.

In addition to the coordinates of the line points, you can set the line width, color, stroke type, and other options (see PolylineOptions).

// Coordinates of the vertices of the polyline.
let points = [
	GeoPoint(latitude: 55.7513, longitude: value: 37.6236),
	GeoPoint(latitude: 55.7405, longitude: value: 37.6235),
	GeoPoint(latitude: 55.7439, longitude: value: 37.6506)
]

// Line settings.
let options = PolylineOptions(
	points: points,
	width: LogicalPixel(value: 2),
	color: DGis.Color.init()
)

// Create and add the line to the map.
let polyline = Polyline(options: options)
objectManager.addObject(object: polyline)

To draw a polygon on the map, create a Polygon object, specifying the required options, and pass it to the addObject() method of the object manager.

Coordinates for the polygon are specified as a two-dimensional array. The first subarray must contain the coordinates of the vertices of the polygon itself. The other subarrays are optional and can be specified to create a cutout (a hole) inside the polygon (one subarray - one polygonal cutout).

Additionally, you can specify the polygon color and stroke options (see PolygonOptions).

// Polygon settings.
let options = PolygonOptions(
	contours: [
		// Vertices of the polygon.
		[
			GeoPoint(latitude: 55.72014932919687, longitude: 37.562599182128906),
			GeoPoint(latitude: 55.72014932919687, longitude: 37.67555236816406),
			GeoPoint(latitude: 55.78004852149085, longitude: 37.67555236816406),
			GeoPoint(latitude: 55.78004852149085, longitude: 37.562599182128906),
			GeoPoint(latitude: 55.72014932919687, longitude: 37.562599182128906)
		],
		// Cutout inside the polygon.
		[
			GeoPoint(latitude: 55.754167897761, longitude: 37.62422561645508),
			GeoPoint(latitude: 55.74450654680055, longitude: 37.61238098144531),
			GeoPoint(latitude: 55.74460317215391, longitude: 37.63435363769531),
			GeoPoint(latitude: 55.754167897761, longitude: 37.62422561645508)
		]
	],
	color: DGis.Color.init(),
	strokeWidth: LogicalPixel(value: 2)
)

// Create and add the polygon to the map.
let polygon = Polygon(options: options)
objectManager.addObject(object: polygon)

To add markers to the map in clustering mode, you must create a MapObjectManager object using MapObjectManager.withClustering(), specifying the map instance, distance between clusters in logical pixels, maximum value of zoom-level, when MapObjectManager in clustering mode, and user implementation of the protocol SimpleClusterRenderer. SimpleClusterRenderer is used to customize clusters in MapObjectManager.

final class SimpleClusterRendererImpl: SimpleClusterRenderer {
	private let image: DGis.Image
	private var idx = 0

	init(
		image: DGis.Image
	) {
		self.image = image
	}

	func renderCluster(cluster: SimpleClusterObject) -> SimpleClusterOptions {
		let textStyle = TextStyle(
			fontSize: LogicalPixel(15.0),
			textPlacement: TextPlacement.rightTop
		)
		let objectCount = cluster.objectCount
		let iconMapDirection = objectCount < 5 ? MapDirection(value: 45.0) : nil
		idx += 1
		return SimpleClusterOptions(
			icon: self.image,
			iconMapDirection: iconMapDirection,
			text: String(objectCount),
			textStyle: textStyle,
			iconWidth: LogicalPixel(30.0),
			userData: idx,
			zIndex: ZIndex(value: 6),
			animatedAppearance: false
		)
	}
}

self.objectManager = MapObjectManager.withClustering(
	map: map,
	logicalPixel: LogicalPixel(80.0),
	maxZoom: Zoom(19.0),
	clusterRenderer: SimpleClusterRendererImpl(image: self.icon)
)

You can control the camera by accessing the map.camera property. See the Camera class for a full list of available methods and properties.

You can change the position of the camera by calling the move() method, which initiates a flight animation. This method has three parameters:

  • position - new camera position (coordinates and zoom level). Additionally, you can specify the camera tilt and rotation (see CameraPosition).
  • time - flight duration in seconds (as TimeInterval).
  • animationType - type of animation to use (CameraAnimationType).

The call will return a Future object, which can be used to handle the animation finish event.

// New position for camera.
let newCameraPosition = CameraPosition(
	point: GeoPoint(latitude: 55.752425, longitude: 37.613983),
	zoom: Zoom(value: 16)
)

// Start the flight animation.
let future = map.camera.move(
	position: newCameraPosition,
	time: 0.4,
	animationType: .linear
)

// Handle the animation finish event.
let cancellable = future.sink { _ in
	print("Camera flight finished.")
} failure: { error in
	print("An error occurred: \(error.localizedDescription)")
}

The current state of the camera (i.e., whether the camera is currently in flight) can be obtained using the state property. See CameraState for a list of possible camera states.

let currentState = map.camera.state

You can subscribe to changes of camera state using the stateChannel.sink property.

// Subscribe to camera state changes.
let connection = map.camera.stateChannel.sink { state in
	print("Camera state has changed to \(state)")
}

// Unsubscribe when it's no longer needed.
connection.cancel()

The current position of the camera can be obtained using the position property (see the CameraPosition structure).

let currentPosition = map.camera.position
print("Coordinates: \(currentPosition.point)")
print("Zoom level: \(currentPosition.zoom)")
print("Tilt: \(currentPosition.tilt)")
print("Rotation: \(currentPosition.bearing)")

You can subscribe to changes of camera position using the positionChannel.sink property.

// Subscribe to camera position changes.
let connection = map.camera.positionChannel.sink { position in
	print("Camera position has changed (coordinates, zoom level, tilt, or rotation).")
}

// Unsubscribe when it's no longer needed.
connection.cancel()

You can add a special marker to the map that will be automatically updated to reflect the current location of the device. To do this, create a data source using the createMyLocationMapObjectSource() function and pass it to the addSource() method of the map.

// Create a data source.
let source = createMyLocationMapObjectSource(
	context: sdk.context,
	directionBehaviour: MyLocationDirectionBehaviour.followMagneticHeading
)

// Add the data source to the map.
map.addSource(source: source)

To remove the marker, call the removeSource() method. You can get the list of active data sources by using the map.sources property.

map.removeSource(source)

You can add a turn-by-turn navigation to your app using the ready-to-use interface components (INavigationView) and the NavigationManager class.

To do that, add a My location marker to the map and create a navigation layer using INavigationViewFactory and NavigationManager.

// Create a map object.
guard let mapFactory = try? sdk.makeMapFactory(options: .default) else {
    return
}

// Add the map view to the view hierarchy.
let mapView = mapFactory.mapView
mapView.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(mapView)
NSLayoutConstraint.activate([
    mapView.leftAnchor.constraint(equalTo: containerView.leftAnchor),
    mapView.rightAnchor.constraint(equalTo: containerView.rightAnchor),
    mapView.topAnchor.constraint(equalTo: containerView.topAnchor),
    mapView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
])

// Add the current location marker to the map.
let locationSource = MyLocationMapObjectSource(
    context: sdk.context,
    directionBehaviour: .followSatelliteHeading,
    controller: createSmoothMyLocationController()
)
let map = mapFactory.map
map.addSource(source: locationSource)

// Create a NavigationManager object.
let navigationManager = NavigationManager(platformContext: sdk.context)

// Attach the map to the NavigationManager.
navigationManager.mapManager.addMap(map: map)

// Create a NavigationViewFactory object.
let navigationViewFactory = sdk.makeNavigationViewFactory()

// Create a navigation view and add it to the view hierarchy above the map.
let navigationView = navigationViewFactory.makeNavigationView(
    map: map,
    navigationManager: navigationManager
)
navigationView.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(navigationView)
NSLayoutConstraint.activate([
    navigationView.leftAnchor.constraint(equalTo: containerView.leftAnchor),
    navigationView.rightAnchor.constraint(equalTo: containerView.rightAnchor),
    navigationView.topAnchor.constraint(equalTo: containerView.topAnchor),
    navigationView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
])

// Add an event handler for the close button.
navigationView.closeButtonCallback = { [weak navigationManager] in
    navigationManager?.stop()
}

You can start navigation in one of three modes: free-drive, turn-by-turn, and the simulated navigation mode.

Additional navigation settings can be changed using the properties of the NavigationManager object.

In free-drive mode, no route will be displayed on the map, but the user will still be informed about speed limits, traffic cameras, incidents, and road closures.

To start navigation in this mode, call the start() method without arguments.

navigationManager.start()

In turn-by-turn mode, a route will be displayed on the map and the user will receive navigation instructions as they move along the route.

To start navigation in this mode, call the start() method and specify a RouteBuildOptions object - arrival coordinates and route settings.

let routeBuildOptions = RouteBuildOptions(
    finishPoint: RouteSearchPoint(
        coordinates: GeoPoint(
            latitude: 55.752425,
            longitude: 37.613983
        )
    ),
    routeSearchOptions: routeSearchOptions
)

navigationManager.start(routeBuildOptions)

Additionally, when calling the start() method, you can specify a TrafficRoute object - a complete route object for navigation. In this case, NavigationManager will use the specified route instead of building a new one.

// Building a route.
self.routeSearchCancellable = routesFuture.sink { routes in
    guard let route = routes.first else { return }

    // Route settings.
    let routeBuildOptions = RouteBuildOptions(
        finishPoint: finishPoint,
        routeSearchOptions: routeSearchOptions
    )
    // Start navigation.
    navigationManager.start(
        routeBuildOptions: routeBuildOptions,
        trafficRoute: route
    )
} failure: { error in
    print("Couldn't build the route: \\(error)")
}

In this mode, NavigationManager will not track the current location of the device. Instead, it will simulate a movement along the specified route.

This mode is useful for debugging.

To use this mode, call the startSimulation() method and specify a RouteBuildOptions object (route settings) and a TrafficRoute object (the route itself).

You can change the speed of the simulated movement using the SimulationSettings.speed property (specified in meters per second).

navigationManager.simulationSettings.speed = 30 / 3.6
navigationManager.startSimulation(
    routeBuildOptions: routeBuildOptions,
    trafficRoute: route
)

To stop the simulation, call the stop() method.

navigationManager.stop()

You can get information about map objects using pixel coordinates. For this, call the getRenderedObjects() method of the map and specify the pixel coordinates and the radius in screen millimeters. The method will return a deferred result (Future) containing information about all found objects within the specified radius on the visible area of the map (an array of RenderedObjectInfo).

An example of a function that takes tap coordinates and passes them to getRenderedObjects():

private func tap(point: ScreenPoint, tapRadius: ScreenDistance) {
	let cancel = map.getRenderedObjects(centerPoint: point, radius: tapRadius).sink(
		receiveValue: {
			infos in
			// First array object is the closest to the coordinates.
			guard let info = infos.first else { return }
			// Process the result in the main thread.
			DispatchQueue.main.async {
				[weak self] in
				self?.handle(selectedObject: info)
			}
		},
		failure: { error in
			print("Error retrieving information: \(error)")
		}
	)
	// Save the result to a property to prevent garbage collection.
	self.getRenderedObjectsCancellable = cancel
}

To work with map styles, you first need to create an IStyleFactory object by calling the makeStyleFactory() method.

let styleFactory = sdk.makeStyleFactory()

To create an SDK-compatible map style, use the Export function in Style Editor and add the downloaded file to your project.

To create a map with a custom style, you need to use the loadResource() or loadFile() method of IStyleFactory and specify the resulting object as the styleFuture map option.

// Create a style factory object.
let styleFactory = sdk.makeStyleFactory()

// Set the map style in map settings.
var mapOptions = MapOptions.default
mapOptions.styleFuture = styleFactory.loadResource(name: "custom_style_file.2gis", bundle: .main)

// Create a map with the specified settings.
let mapFactory = sdk.makeMapFactory(options: mapOptions)

The loadResource() and loadFile() methods return a deferred result (Future) so as not to delay the loading of the map. If the style has already been loaded (see the next section for more details), you can convert it into a Future object using the makeReadyValue() method.

var mapOptions = MapOptions.default
mapOptions.styleFuture = Future.makeReadyValue(style)

To change the style of an existing map, use the setStyle() method.

Unlike the styleFuture map option, setStyle() accepts a fully loaded Style object instead of a Future object. Therefore, setStyle() should be called after the Future has been resolved.

// Create a style factory object.
let styleFactory = sdk.makeStyleFactory()

// Load a new map style. The loadFile() method only accepts the file:// URI scheme.
self.cancellable = styleFactory.loadFile(url: styleFileURL).sink(
	receiveValue: { [map = self.map] style in
		// After the style has been loaded, use it to change the current map style.
		map.setStyle(style: style)
	},
	failure: { error in
		print("Failed to load style from <\(fileURL)>. Error: \(error)")
	})

Each map style can contain several themes that you can switch between without having to load an additional style. You can specify which theme to use by setting the appearance map option when creating the map.

In iOS 13.0 and later, you can also use the automatic switching between light and dark themes (see Dark Mode).

// Map settings.
var mapOptions = MapOptions.default

// Name of the light theme.
let lightTheme: Theme = "day"

// Name of the dark theme.
let darkTheme: Theme = "night"

if #available(iOS 13.0, *) {
	// Automatically switch between the light and dark themes.
	mapOptions.appearance = .automatic(light: lightTheme, dark: darkTheme)
} else {
	// Use only the light theme.
	mapOptions.appearance = .universal(lightTheme)
}

// Create a map with the specified settings.
let mapFactory = sdk.makeMapFactory(options: mapOptions)

To change the theme after the map has been created, use the appearance property of IMapView:

// Get the map view.
let mapView = mapFactory.mapView

// Change the theme to dark.
mapView.appearance = .universal(darkTheme)

To customize the map gesture recognizer, you need to set the IMapGestureView implementation in IMapView or IMapGestureViewFactory implementation in MapOptions. If no implementations are specified, the default implementations will be used. An example of such recognizer is available here.