MetamapsDeveloper Docs

Public SDK installation guide

Metamaps iOS SDK Developer Guide

This guide adds the Metamaps map view to an iOS app from a local Swift package, configures it, and shows the basic calls an app makes. Indoor positioning is optional; step 8 covers it. The SDK requires iOS 16 or later and Xcode 26 or later.

1. Get the SDK package

Place the SDK package in a fixed directory inside your app repository, for example Vendor/metamaps-ios. The directory must keep Package.swift and Artifacts/ side by side.

Clone the public repository and copy its ios/ directory (run from your app repository):

git clone https://github.com/boldright/Metamaps-SDK.git ../Metamaps-SDK
mkdir -p Vendor
cp -R ../Metamaps-SDK/ios Vendor/metamaps-ios

To get a specific version, check out its tag, for example git -C ../Metamaps-SDK checkout v0.8.0.

Keeping the directory name fixed means Xcode's package reference survives SDK updates. Step 10 describes how to replace the package.

2. Add the package in Xcode

  1. Choose File > Add Package Dependencies... > Add Local....
  2. Select the directory that contains Package.swift.
  3. Add the Metamaps product to your app target.

If your app is itself a Swift package, add the dependency with a relative path:

dependencies: [
    .package(path: "../Vendor/metamaps-ios"),
]

3. Add the view with UIKit

Use MetamapsMapView on the main actor. Call dispose() on the main actor when the screen closes, not from deinit. MetamapsMapViewDelegate receives map readiness and error events. In this example, MapViewController implements the delegate itself.

import Metamaps
import UIKit

@MainActor
final class MapViewController: UIViewController, MetamapsMapViewDelegate {
    private var metamapsView: MetamapsMapView?
    private var loadTask: Task<Void, Never>?
    private var isMapReady = false

    override func viewDidLoad() {
        super.viewDidLoad()

        let mapView = MetamapsMapView(configuration: .init(
            mapSlug: "example",
            language: "en"
        ))
        mapView.delegate = self
        mapView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(mapView)
        NSLayoutConstraint.activate([
            mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            mapView.topAnchor.constraint(equalTo: view.topAnchor),
            mapView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        ])
        metamapsView = mapView

        loadTask = Task { @MainActor in
            do {
                try await mapView.load()
            } catch is CancellationError {
                // The screen closed while loading.
            } catch {
                // Show your recovery UI.
            }
        }
    }

    func metamapsMapView(_ mapView: MetamapsMapView, didReceive event: MetamapsMapViewEvent) {
        switch event {
        case .ready:
            isMapReady = true
        case .loadState(let state) where state != .loaded:
            // Commands throw again from a new page load, a failure, or disposal until the next .ready.
            isMapReady = false
        case .error(let error):
            // Show your recovery UI.
            print(error.localizedDescription)
        default:
            // Keep a default branch: SDK updates may add events.
            break
        }
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        guard isBeingDismissed || isMovingFromParent || navigationController?.isBeingDismissed == true else {
            return
        }
        loadTask?.cancel()
        metamapsView?.dispose()
        metamapsView = nil
        isMapReady = false
    }
}

Set mapSlug to the URL identifier of the public map configured in the Metamaps console. "example" is a placeholder that only lets the app build; replace it with the slug you were given before running the app.

Keep MetamapsMapView as a property of the view controller that shows it. You do not need to call dispose() when the app moves to the background or another screen temporarily covers the map. Cancel the loading task and call dispose() on the main actor only when the screen closes and the view controller is released.

4. Add the Info.plist keys

Add these four keys to your app's Info.plist, even if you only show maps:

<key>NSMicrophoneUsageDescription</key>
<string>The microphone lets you search the map and ask questions by voice.</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>Speech recognition turns your voice into map searches and questions.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Your location is used to show where you are on the map and to find routes.</string>
<key>NSMotionUsageDescription</key>
<string>Motion data keeps your location up to date while you walk.</string>
  • The microphone and speech recognition keys are used by the map's voice input. Without either one, the map hides its microphone button. The system asks for these permissions the first time the user taps the microphone.
  • If the user denies location or microphone permission, the map asks the user to allow it in the device settings. The map view does not report the denial to your app.
  • The location key is used when the map shows the user's location: by GPS location on maps that enable it, and by indoor positioning (step 8). On a map with GPS location enabled, the system asks for location permission the first time the user taps the location button in the map, even in an app that only shows maps.
  • The motion key is used only by indoor positioning, and the system asks for it only when positioning starts.
  • App Store Connect checks for the location and motion keys because the SDK links Core Location and Core Motion.

The SDK does not use Core Bluetooth directly, so it needs neither NSBluetoothAlwaysUsageDescription nor the background location mode.

5. Receive events

MetamapsMapView sends map readiness and errors to the delegate as MetamapsMapViewEvent. The main events are:

  • .ready: the web map accepts setLanguage(_:), selectFloor(_:), showSpot(_:), and setDestination(_:).
  • .error: loading the map or a configuration value failed, or the web map could not complete an operation. For the last case the code is mapOperationFailed; the map stays usable, so do not reload it.

load() finishes when the web content has loaded, which is earlier than .ready. As in the example above, wait for .ready before calling methods that control the displayed map.

The map can also fail after it has loaded, for example when the web content process ends. The view then sends .loadState(.failed) and an error whose userAction is .retry, and the map stays blank until you call reload().

Back navigation

When the user opens a spot, the map adds a step to its history, as the web map does in a browser. iOS has no system back button, but if your screen has its own back control, let it close that step first. canGoBack is true while the map has such a step, .canGoBackChanged reports its changes, and goBack() closes the step:

// In the action of your back button.
if metamapsView?.goBack() != true {
    navigationController?.popViewController(animated: true)
}

The back button of a UINavigationController and its edge swipe pop your screen without calling your code. To let them close the map's step first, show your own back button (set navigationItem.hidesBackButton) and disable navigationController?.interactivePopGestureRecognizer while canGoBack is true.

The map's share buttons open the iOS share sheet through WebKit. Your app needs no code for this.

6. Change the display language

Set the initial language with MetamapsMapViewConfiguration.language, using a BCP 47 language tag such as ja, en, or zh-Hans. To change the language of a displayed map, call MetamapsMapView.setLanguage(_:). You do not need to recreate the view; the web map reloads in the selected language.

Add this method to MapViewController:

@MainActor
func changeMapLanguage(to language: String) {
    guard isMapReady else { return }
    do {
        try metamapsView?.setLanguage(language)
    } catch {
        // Show your recovery UI.
    }
}

The available languages depend on the map's publishing settings. A language the map does not publish produces an .error event with mapOperationFailed. Pass a specific published language tag to setLanguage(_:). To return to automatic selection by device language, create a new map view with language: nil or language: "auto".

7. Query parameters

Appendix: map view query parameters lists the public parameters for the initial view, their value ranges, and how to change them after the map is displayed. For example, to open a specific spot, pass it in additionalQuery:

let configuration = MetamapsMapViewConfiguration(
    mapSlug: "example",
    language: "en",
    additionalQuery: [
        "spot": "6Y6SSBY5",
    ]
)

8. Indoor positioning (optional)

Indoor positioning shows the user's current location on maps whose facility has installed beacons and enabled positioning in Metamaps. For other maps, the map view requests no permissions and downloads no positioning data, so map-only apps can skip this step.

Location permission is required for positioning. Motion & Fitness is optional and is used only for step counting; if the user denies it, positioning continues without steps.

When positioning starts

positioningStartTrigger decides when positioning starts. If a required permission has not been granted when positioning starts, the OS shows its permission dialog.

ValueBehavior
.userActionStarts when the user taps the location button in the map
.automaticStarts automatically once the map is ready
.automaticWhenAuthorized (default)Starts automatically once the map is ready, but only if every required permission is already granted
let configuration = MetamapsMapViewConfiguration(
    mapSlug: "example",
    positioningStartTrigger: .userAction
)

To use your own location button, call requestPositioningAuthorization() and startPositioning() from the app. Xcode Quick Help documents every public type.

positioningStartTrigger only decides when a start request happens. positioningPolicy decides whether the SDK handles the request or passes it to your app. The default .userInitiated lets the SDK request permissions and start positioning, .hostControlled only sends the .positioningStartRequested event to your app, and .disabled ignores start requests. With .hostControlled, your app keeps control of when and how permissions are requested even when automatic start is enabled.

To show positioning with your own map instead of the map view, use the MetamapsPositioning product and MetamapsPositioningClient.

9. Build

In Xcode, select your app scheme and an iOS Simulator, then choose Product > Build or Product > Run. To check the same setup from a terminal, run:

xcodebuild \
  -project YourApp.xcodeproj \
  -scheme YourApp \
  -configuration Debug \
  -destination 'generic/platform=iOS Simulator' \
  -derivedDataPath ./DerivedData \
  clean build

10. Update the SDK

Check out the new version in your clone of the public repository and replace the whole Vendor/metamaps-ios/ directory. Do not change Xcode's package reference, Info.plist, or your code.

git -C ../Metamaps-SDK fetch --tags
git -C ../Metamaps-SDK checkout v<version>
rm -rf Vendor/metamaps-ios
cp -R ../Metamaps-SDK/ios Vendor/metamaps-ios

Replace <version> with the version to install, for example 0.8.0.

Do not skip rm -rf. If Vendor/metamaps-ios still exists, cp -R copies the new directory into it. Xcode then keeps reading the old Vendor/metamaps-ios/Package.swift and Artifacts/, and the build succeeds with the old SDK without any error or warning.

After replacing the package, choose Product > Clean Build Folder in Xcode and build again. The local package resolves by the fixed directory name from step 1, so you do not need to re-add the package. If Xcode keeps showing old content, choose File > Packages > Reset Package Caches.

To check the installed version, read MetamapsSDK.version at run time:

import Metamaps

print(MetamapsSDK.version)

Read CHANGELOG.md for the changes in each version. A version that changes a default value can change your app's behavior even if you do not change your code.

Search documentation

Search MCP, API, SDK, and Web Embed guides.