PowerMap iOS SDK
A professional-grade, high-performance Map SDK for iOS.
Step 1: Add SDK to Project
- Download the
PowerMapSDK.xcframework.zipfile from: http://download.powermap.live/#sdks - Extract the
.zipfile to get thePowerMapSDK.xcframeworkfolder - Drag the
PowerMapSDK.xcframeworkfile into your Xcode project - In General → Frameworks, Libraries, and Embedded Content, set it to Embed & Sign
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"
)
}
}
Secrets.swift file that is added to .gitignore. See the powermap-ios-sdk-demo project for an example.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:
PowerMapControllerprovides 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.
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)?
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)
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!")
}
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 ?? "")")
}
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
)
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" |
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()
}
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()
}
}
}
}
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)")
}
}
PowerMapError cases conform to LocalizedError, so error.localizedDescription always returns a human-readable message prefixed with PowerMapException[ERROR_CODE]: — making log filtering straightforward.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.