Installation

Looking for a complete implementation? Check out the iOS SDK Demo Repository on GitHub.

Step 1: Add SDK to Project

  1. Download the PowerMapSDK.xcframework.zip file from: http://download.powermap.live/#sdks
  2. Extract the .zip file to get the PowerMapSDK.xcframework folder
  3. Drag the PowerMapSDK.xcframework file into your Xcode project
  4. In General → Frameworks, Libraries, and Embedded Content, set it to Embed & Sign
All dependencies (including the Map Engine) are included in the SDK. No additional dependencies are required.

Step 2: Configure API Key

Insert the initialize code into your AppDelegate or App struct:

import PowerMapSDK

@main
struct MyApp: App {
    init() {
        PowerMapSDK.shared.initialize(
            mapApiKey: "YOUR_MAP_API_KEY",
            clientId: "YOUR_CLIENT_ID",
            clientSecret: "YOUR_CLIENT_SECRET"
        )
    }
}
For security reasons, do not hardcode keys directly in your code — we recommend creating a Secrets.swift file that is added to .gitignore. See the powermap-ios-sdk-demo project for an example.

Architecture Overview

The PowerMap iOS SDK is built with a Layered Facade Architecture, ensuring that high-performance native rendering is wrapped in an easy-to-use Swift API.

graph TD
    App[iOS App] --> PowerMap[MLNMapView]
    PowerMap --> Controller[PowerMapController]
    
    subgraph "Facade Layer"
        Controller
    end

    subgraph "Manager Layer"
        Controller --> MarkerMgr[MarkerManager]
        Controller --> SearchMgr[SearchManager]
        Controller --> RouteMgr[RoutingManager]
        Controller --> GeomMgr[GeometryManager]
        Controller --> TrackingMgr[TrackingManager]
    end

    subgraph "Native Engine"
        PowerMap --> NativeEngine[High-Performance Metal Rendering]
    end
  • Unified Facade: PowerMapController provides a single entry point for all features, matching the Web and Flutter DX.
  • Native Rendering: Polygons, Lines, and Symbols are rendered via the native Metal rendering engine for fluid performance.
  • Async/Await Native: Fully async APIs (search, addRoute, etc.) built natively with Swift 5.5 concurrency.

Quick Start

Get a map up and running in less than 2 minutes.

import UIKit
import PowerMapSDK

class MapViewController: UIViewController, MLNMapViewDelegate {
    var mapView: MLNMapView!
    var controller: PowerMapController!

    override func viewDidLoad() {
        super.viewDidLoad()
        
        mapView = MLNMapView(frame: view.bounds)
        mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        view.addSubview(mapView)
        
        // 1. Initialize the Facade Controller
        controller = PowerMapController(mapView: mapView)
        
        // 2. Set Map Style
        controller.setStyle(.dark)
        
        // 3. Move Camera
        controller.moveCamera(target: CLLocationCoordinate2D(latitude: 13.7563, longitude: 100.5018), zoom: 14.0)
        
        // 4. Listen to events
        controller.onMapClick = { coord in
            print("Tapped at: \(coord)")
        }
    }
}

PowerMapView Initialization

Parameter Type Default Description
style PowerMapStyle .th Base tile style (.th, .en, .dark, .gray).
onMapReady (PowerMapController) -> Void Required Triggered when the Metal context is fully initialized.

Global Event Callbacks (on PowerMapController)

  • onMapClick: ((CLLocationCoordinate2D) -> Void)?
  • onMapLongClick: ((CLLocationCoordinate2D) -> Void)?
  • onPoiTapped: ((MLNFeature) -> Void)?
  • onMarkerTapped: ((MLNAnnotation) -> Void)?
  • onCameraMove: ((CLLocationCoordinate2D) -> Void)?
  • onCameraIdle: (() -> Void)?
  • onError: ((Error) -> Void)?

Camera & Viewport Control

Direct viewport manipulation matching the Android SDK.

Movement Methods

Method Arguments Description
moveCamera(target:zoom:tilt:bearing:animated:) CLLocationCoordinate2D, Double?, Double?, Double?, Bool Smooth fly-to animation.
moveCamera(target:zoom:tilt:bearing:durationMs:) ..., durationMs: Int Timed animation — mirrors Android.
flyTo(_:zoom:durationMs:) CLLocationCoordinate2D, Double, Int Alias for moveCamera with duration.
moveTo(_:zoom:) CLLocationCoordinate2D, Double Instant move, no animation.
navMoveCamera(target:zoom:tilt:bearing:) CLLocationCoordinate2D, Double, Double, Double Navigation-safe atomic snap.
flyToNavigation(target:bearing:completion:) CLLocationCoordinate2D, Double, () -> Void Google Maps-style intro animation.
fitRouteBounds(_:paddingPx:durationMs:) [CLLocationCoordinate2D], Double, Int Zoom-to-fit route viewport.
zoomIn() / zoomOut() - ±1 zoom level.
setTilt(_:) / setBearing(_:) Double Set pitch or heading.
tiltUp() / tiltDown() - 60° tilt / reset to 2D.

State Properties

Method Returns Description
getCenter() CLLocationCoordinate2D? Current map center.
getZoom() Double? Current zoom level.
getBearing() Double? Current heading (degrees).
getTilt() Double? Current camera pitch.
controller.moveCamera(
    target: CLLocationCoordinate2D(latitude: 13.7649, longitude: 100.5383),
    zoom: 17.0,
    tilt: 60.0,
    bearing: 90.0
)

// Zoom to fit a route
controller.fitRouteBounds(routeCoordinates, paddingPx: 80, durationMs: 600)

Marker Management

Core Marker Methods

Method Arguments Description
addMarker(position:color:iconImage:size:title:) CLLocationCoordinate2D, String?, String?, Float?, String? Adds a Metal-rendered symbol marker.
clearMarkers() - Removes all markers.
onMarkerTapped ((MLNAnnotation) -> Void)? Callback when a marker is tapped.

High Performance GL Markers

Best for large datasets. Rendered directly by the GPU via Metal.

let marker = controller.markers.addMarker(
    position: CLLocationCoordinate2D(latitude: 13.76, longitude: 100.50),
    color: "#f97316"
)

controller.onMarkerTapped = { annotation in
    print("Marker tapped!")
}

Routing & Optimization

Calculate logistics-grade paths. Automatically draws the route polyline on the map.

addRoute() Options

Option Type Default Description
start CLLocationCoordinate2D Required Origin coordinate.
destination CLLocationCoordinate2D Required Final destination coordinate.
waypoints [CLLocationCoordinate2D] [] Stops between origin and destination (Max 25).
optimize Bool false If true, applies TSP logic to reorder waypoints.
profile String "driving" Modes: "driving", "walking", "cycling".
language String "th" Output instruction language: "th", "en".
Task {
    let route = try await controller.addRoute(
        start: CLLocationCoordinate2D(latitude: 13.7468, longitude: 100.5346),
        destination: CLLocationCoordinate2D(latitude: 13.7649, longitude: 100.5383),
        optimize: true
    )
    print("Route calculated: \(route?.distanceStr ?? "")")
}

Turn-by-Turn Navigation & Simulation

A full-featured navigation engine with 60 FPS simulation, TTS voice guidance, GPS snap-to-route, automatic rerouting, and real-time state publishing.

Quick Start

import SwiftUI
import PowerMapSDK

struct MapView: View {
    @StateObject var navigator = PowerMapNavigator()
    var controller: PowerMapController?

    var body: some View {
        // ...
        .onReceive(navigator.$state) { state in
            guard let state else { return }
            print("Turn in: \(Int(state.distanceToNextManeuver))m")
            print("Progress: \(Int(state.progress * 100))%")
            print("Instruction: \(state.currentInstruction ?? "")")
        }
        .task {
            navigator.setController(controller)
            if let route = try? await controller?.routing.addRoute(
                start: CLLocationCoordinate2D(latitude: 13.7468, longitude: 100.5346),
                destination: CLLocationCoordinate2D(latitude: 13.7649, longitude: 100.5383)
            ) {
                navigator.startNavigation(route: route, mode: .simulation)
            }
        }
    }
}

Navigation Mode

Mode Description
.simulation Auto-drives a 3D vehicle along the route at 60 FPS. Speed follows actual route step durations × speedMultiplier.
.gps Awaits live CoreLocation callbacks. Uses snap-to-route, GPS hysteresis, automatic rerouting, and compass fusion.

Playback Controls

// Pause / Resume
navigator.pauseSimulation()
navigator.resumeSimulation()

// Speed multiplier (1x, 2x, 3x, 5x recommended)
navigator.setSpeedMultiplier(3)

// Follow vehicle (unlock/re-lock camera)
navigator.followVehicle = false  // Free-roam camera
navigator.followVehicle = true   // Re-lock

// Stop navigation
navigator.stopNavigation()

Vehicle Arrow Customization

// By UIColor
controller.setVehicleColor(UIColor.systemBlue)

// By hex string
controller.setVehicleColor("#4285F4")

NavigationState Fields

Observe via navigator.$state (Combine @Published):

Property Type Description
currentPosition CLLocationCoordinate2D Current vehicle position (real or simulated).
bearing Double Vehicle heading in degrees (0–360).
speedMps Double Current speed in meters/second.
distanceToNextManeuver Double Meters until the next turn.
currentStepIndex Int Index into route.steps[] for the current leg.
currentInstruction String? Human-readable turn instruction.
isOffRoute Bool true if the vehicle has drifted >20m from the route for >3s.
progress Float Route completion fraction (0.0 → 1.0).

@Published Properties (SwiftUI / Combine)

PowerMapNavigator is annotated with @MainActor. All @Published property updates are guaranteed to happen on the main thread — safe to bind directly to SwiftUI views.
Property Type Description
state NavigationState? Full real-time navigation state. nil when not navigating.
isPaused Bool true when simulation is paused.
speedMultiplier Int Current simulation speed multiplier (default: 1).
followVehicle Bool true when camera is locked to vehicle. Always true at navigation start.
currentMode NavigationMode .simulation or .gps — read-only after start.

Geometry & Vector Data

The SDK features a robust GeometryManager that natively handles GeoJSON Polygon, LineString, or Point types.

Method Execution Description
importGeoJson() sync Parses GeoJSON and automatically renders layers based on the geometry type.
addPolygonLayer() sync Renders Fills and Outlines. Mix with fillOpacity to create interactive Geofences.
addPolylineLayer() sync Renders colored paths/lines with customizable thickness.
addIndoorExtrusionLayer() sync Automatically extrudes Polygons into 3D assuming data has a height property.
// 1. Prepare your Geometry Data (GeoJSON format: Lng, Lat)
let geofenceData: [String: Any] = [
    "type": "Feature",
    "geometry": [
        "type": "Polygon",
        "coordinates": [[
            [100.534, 13.746], [100.538, 13.746], 
            [100.538, 13.749], [100.534, 13.749], 
            [100.534, 13.746] // Must close the loop
        ]]
    ]
]

// 2. Inject Data into the Map Source
controller.geometry.addGeoJson(sourceId: "my-geofence", geoJson: geofenceData)

// 3. Apply Visual Styles to that Source ID
controller.geometry.addPolygonLayer(
    sourceId: "my-geofence",
    fillColor: .blue, 
    fillOpacity: 0.5
)

Search & Geocoding

Safe, async/await driven Geocoding with debouncing and LRU Caching (50 items) built-in.

Search Methods

Method Arguments Description
search() query, location?, lang? Standard forward search. LRU cached.
searchByCategory() categoryId, location, dist?, lang? Searches for nearby POIs by category type code (e.g. "2100" = Restaurant).
autocompleteDebounced() query, delay, location?, lang?, onResult Safe to bind to TextField. Cancels previous call automatically.
reverseGeocode() location, lang? Converts coordinates into a structured address.
clearSearchCache() - Clears the internal LRU cache manually.

Language Detection (lang Parameter)

lang value Endpoint When to use
"th" (default) /api/v2/map/address_th Query contains Thai characters only
"en" /api/v2/map/address_en Query contains Latin/English characters only
"mixed" /api/v2/map/address Query contains both Thai and English
// 1. Debounced Autocomplete
controller.autocompleteDebounced(query: query, delay: 0.5, lang: "th") { results in
    if let first = results.first {
        print("Found: \(first.name)")
    }
}

// 2. One-shot forward search
Task {
    let results = try await controller.search(query: "Central World", lang: "en")
    print("Found: \(results.first?.name ?? "")")
}

// 3. Reverse Geocode (Lat/Lng to Address)
Task {
    if let address = await controller.reverseGeocode(location: CLLocationCoordinate2D(latitude: 14.2558, longitude: 100.9810)) {
        print("Address: \(address.name)")
    }
}

// 4. Category Search (Nearby POIs by type)
Task {
    if let myLocation = await controller.getCurrentLocation() {
        let restaurants = try await controller.searchByCategory(
            categoryId: "2100",
            location: myLocation,
            dist: 5
        )
        print("Found \(restaurants.count) nearby restaurants!")
    }
}

Category IDs Reference

Category categoryId
Restaurant "2100"
Fuel Station "4500"
Hotel "4300"
Café "2200"
Supermarket "4600"

Real-Time Tracking

Highly integrated with CoreLocation for battery-efficient GPS positioning.

Core Methods

Method Returns Description
enableFollowMode() async Locks camera on device GPS location.
enableFollowModeWithCompass() async Locks camera + rotates to match device heading with 3D tilt.
disableFollowMode() sync Unlocks camera, stops tracking.
getCurrentLocation() async CLLocationCoordinate2D? One-shot GPS request.
requestPermission() async Bool Checks and requests location permission automatically.
showUserLocation(_:) sync Enables/disables the visual blue puck indicator.
disposeTracking() sync Releases GPS resources.
Task {
    // 1. One-shot: Get current coordinates
    let myLocation = await controller.getCurrentLocation()

    // 2. Navigation mode with compass heading (3D view)
    await controller.enableFollowModeWithCompass()

    // 3. Stop tracking later
    controller.disableFollowMode()
}
You must call disposeTracking() when the App enters the background to stop the GPS stream and save battery life.
@main
struct MyApp: App {
    @Environment(\.scenePhase) private var scenePhase
    @StateObject private var mapViewModel = MapViewModel()

    var body: some Scene {
        WindowGroup {
            ContentView().environmentObject(mapViewModel)
        }
        .onChange(of: scenePhase) { phase in
            if phase == .background || phase == .inactive {
                mapViewModel.controller?.disposeTracking()
            }
        }
    }
}

Error Handling

The SDK exposes a unified PowerMapError enum that conforms to LocalizedError, providing consistent error codes across iOS, Android, and Flutter platforms.

Error Cases

Case Error Code Description
.notInitialized(msg) STYLE_LOAD_FAILED SDK used before initialize() is called.
.network(msg, error?) NETWORK_ERROR Network request failed.
.auth(msg) AUTH_FAILED API key or Client ID/Secret is invalid or expired.
.serverError(msg, code) SERVER_ERROR Server responded with a non-2xx HTTP status code.
.routeNotFound ROUTE_NOT_FOUND Routing engine could not find a valid path.
.invalidRequest(msg) INVALID_REQUEST Malformed parameters passed to an SDK method.
.styleLoadFailed(msg) STYLE_LOAD_FAILED The map tile style URL failed to load.
.mapInternal(msg) MAP_INTERNAL_ERROR Unexpected internal error in the map rendering engine.

Standard Error Code Constants

PowerMapError.NETWORK         // "NETWORK_ERROR"
PowerMapError.AUTH            // "AUTH_FAILED"
PowerMapError.INVALID_REQUEST // "INVALID_REQUEST"
PowerMapError.NOT_FOUND       // "NOT_FOUND"
PowerMapError.STYLE_LOAD      // "STYLE_LOAD_FAILED"
PowerMapError.MAP_INTERNAL    // "MAP_INTERNAL_ERROR"
PowerMapError.ROUTE_NOT_FOUND // "ROUTE_NOT_FOUND"
PowerMapError.SERVER_ERROR    // "SERVER_ERROR"

Global Error Callback

controller.onError = { error in
    guard let mapError = error as? PowerMapError else { return }

    switch mapError {
    case .notInitialized(let msg):
        print("SDK not ready: \(msg)")
    case .network(let msg, let underlying):
        print("Network error: \(msg)")
    case .auth(let msg):
        print("Authentication failed: \(msg)")
    case .serverError(let msg, let code):
        print("Server error \(code): \(msg)")
    case .routeNotFound:
        print("No route available.")
    case .invalidRequest(let msg):
        print("Bad request: \(msg)")
    case .styleLoadFailed(let msg):
        print("Style failed to load: \(msg)")
    case .mapInternal(let msg):
        print("Internal map error: \(msg)")
    }
}

Inline try/catch for Async APIs

Task {
    do {
        let route = try await controller.addRoute(
            start: CLLocationCoordinate2D(latitude: 13.7468, longitude: 100.5346),
            destination: CLLocationCoordinate2D(latitude: 13.7649, longitude: 100.5383)
        )
        print("Route: \(route?.distanceStr ?? "")")
    } catch PowerMapError.routeNotFound {
        print("No route found — try different waypoints.")
    } catch PowerMapError.network(let msg, _) {
        print("Network issue: \(msg)")
    } catch {
        print("Unexpected error: \(error.localizedDescription)")
    }
}
All PowerMapError cases conform to LocalizedError, so error.localizedDescription always returns a human-readable message prefixed with PowerMapException[ERROR_CODE]: — making log filtering straightforward.


API Reference

Browse the generated DocC documentation for a comprehensive API surface. Start with PowerMapView and PowerMapController as your primary integration points.


© 2026 PowerMap Development Team. All rights reserved.