Appearance
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, nameArea & 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()Navigation
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)Search
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
| Event | Payload | Description |
|---|---|---|
.ready | — | Map fully loaded |
.levelChange | Level | User or SDK switched levels |
.areaClick | Area | User tapped an area |
.markerClick | Marker | User tapped a marker |
.areaSelect | Area | Area selected/focused |
.markerSelect | Marker | Marker selected/focused |
.routeBuilt | RouteResult, levelId | Route calculated |
.routeNotFound | — | No route found |
.languageChange | String | Language switched via setLanguage() |
.error | String | SDK 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.