Skip to content

iOS SDK

Embed interactive Layota maps into your iOS app with a native Swift API.

Requirements

  • iOS 15+
  • Swift 5.9+
  • Xcode 15+

Installation

Source distribution for now

The iOS SDK is not published to a public Swift Package Manager registry yet — contact us to get the package source. A public SPM release is planned.

The SDK ships as a standard Swift package (LayotaMapSDK). Once you have the source, add it as a local package:

In Xcode: File → Add Package Dependencies → Add Local… and select the package folder.

Or reference it from your Package.swift:

swift
dependencies: [
    .package(path: "../LayotaMapSDK")
]

Quick Start

UIKit

swift
import LayotaMapSDK

class MapViewController: UIViewController {

    let mapView = LayotaMapView(options: LayotaMapOptions(
        projectId: "your-project-id",
        apiKey: "sk_..."
    ))

    override func viewDidLoad() {
        super.viewDidLoad()

        view.addSubview(mapView)
        mapView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            mapView.topAnchor.constraint(equalTo: view.topAnchor),
            mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            mapView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        ])

        let map = mapView.map

        map.on(.areaClick) { event in
            if case .areaClick(let area) = event {
                print("Tapped area:", area.title)
            }
        }

        Task {
            try await map.ready()
            map.selectArea("coffee-shop").focusOn("coffee-shop")
        }
    }
}

SwiftUI

swift
import SwiftUI
import LayotaMapSDK

struct ContentView: View {
    @State private var map: LayotaMap?

    var body: some View {
        LayotaMapSwiftUI(
            options: LayotaMapOptions(
                projectId: "your-project-id",
                apiKey: "sk_...",
                language: "en"
            ),
            map: $map
        )
        .ignoresSafeArea()
        .task {
            map?.on(.areaClick) { event in
                if case .areaClick(let area) = event {
                    print("Tapped:", area.title)
                }
            }
            try? await map?.ready()
            map?.selectArea("entrance")
        }
    }
}

Configuration

swift
let options = LayotaMapOptions(
    projectId: "proj-uuid",        // Required
    apiKey: "sk_...",              // Required if project has allowed domains
    theme: .dark,                  // .light | .dark | .blue | .auto
    language: "ru",                // Language code
    levelId: "floor-2",           // Start on this level (id, key, or name)
    areaId: "cafe",               // Focus this area on load
    markerId: "exit-north",       // Focus this marker on load
    baseURL: URL(string: "https://layota.app")  // Override for self-hosted
)

Methods

Lifecycle

swift
// Wait for the map to finish loading
try await map.ready()

// Clean up WKWebView and all event listeners
map.destroy()

Level Control

swift
// Switch level by id, key (slug), or name
map.selectLevel("floor-2")

// Fetch all visible levels
let levels: [Level] = try await map.getLevels()
// Level: id, name

Area & Marker Selection

swift
// Select by id, key (slug), or title — opens the info card
map.selectArea("cafe")
map.selectArea("cafe", levelId: "floor-1")

map.selectMarker("exit-north")
map.selectMarker("exit-north", levelId: "floor-1")

// Close the info card and deselect
map.clearSelection()
swift
// Pan and zoom to area or marker — switches level if needed
map.focusOn("cafe")

// Reset zoom and pan to default
map.resetView()

// Set zoom (throws LayotaError.invalidScale if scale <= 0)
try map.setZoom(1.5)
swift
// Open the search panel and filter results by query
map.search("coffee")

// Clear the search
map.search("")

Routing

swift
let origin = RouteEntity(id: "entrance", type: .area, levelId: "floor-1")
let destination = RouteEntity(id: "cafe", type: .area, levelId: "floor-2")

map.buildRoute(from: origin, to: destination)
map.clearRoute()

Language

swift
// Get available languages
let (languages, current) = try await map.getLanguages()
// Language: code, name
// current: currently active language code

// Switch language at runtime
map.setLanguage("de")

Areas & Markers Data

swift
// All areas across all levels
let allAreas: [Area] = try await map.getAreas()

// Areas for a specific level (by id, key, or name)
let floorAreas: [Area] = try await map.getAreas(levelId: "floor-1")

// Same for markers
let allMarkers: [Marker] = try await map.getMarkers()
let floorMarkers: [Marker] = try await map.getMarkers(levelId: "floor-1")

Project Data

swift
let project: ProjectInfo = try await map.getProject()
// ProjectInfo: id, levels[]
// ProjectLevel: id, name, areas[], markers[]

Method Chaining

swift
try map.selectLevel("floor-1")
       .selectArea("cafe")
       .focusOn("cafe")
       .setZoom(1.5)

Events

Subscribe

swift
let subscription = map.on(.areaClick) { event in
    if case .areaClick(let area) = event { ... }
}

map.on(.levelChange) { event in
    if case .levelChange(let level) = event { ... }
}

map.on(.routeBuilt) { event in
    if case .routeBuilt(let route, let levelId) = event { ... }
}

map.on(.error) { event in
    if case .error(let message) = event { print("SDK error:", message) }
}

Subscribe Once

swift
map.once(.ready) { _ in print("Map is ready") }

Unsubscribe

swift
// Cancel a specific subscription
subscription.cancel()

// Remove ALL handlers for an event type
map.off(.areaClick)

Event Reference

EventPayloadDescription
.readyMap fully loaded
.levelChangeLevelUser or SDK switched levels
.areaClickAreaUser tapped an area
.markerClickMarkerUser tapped a marker
.areaSelectAreaArea selected/focused
.markerSelectMarkerMarker selected/focused
.routeBuiltRouteResult, levelIdRoute calculated
.routeNotFoundNo route found
.languageChangeStringLanguage switched via setLanguage()
.errorStringSDK error

Architecture

The SDK loads https://layota.app/embed/{projectId}?native=1 in a WKWebView. The ?native=1 parameter enables the native bridge — events are sent via window.webkit.messageHandlers.layota instead of window.parent.postMessage.

Commands are sent from native to the web page via WKWebView.evaluateJavaScript. Commands issued before the map finishes loading are queued and flushed once READY is received.

Layota Documentation