🎯

mapbox-ios-patterns

🎯Skill

from mapbox/mapbox-agent-skills

VibeIndex|
What it does

Enables seamless Mapbox Maps SDK integration in iOS apps using Swift, SwiftUI, and UIKit with best practices for lifecycle management and performance.

πŸ“¦

Part of

mapbox/mapbox-agent-skills(9 items)

mapbox-ios-patterns

Installation

Quick InstallInstall with npx
npx add-skill mapbox/mapbox-agent-skills
Quick InstallInstall with npx
npx add-skill mapbox/mapbox-agent-skills --skill mapbox-web-performance-patterns
Quick InstallInstall with npx
npx add-skill mapbox/mapbox-agent-skills --list
Quick InstallInstall with npx
npx add-skill mapbox/mapbox-agent-skills -a cursor
Quick InstallInstall with npx
npx add-skill mapbox/mapbox-agent-skills -a vscode

+ 2 more commands

πŸ“– Extracted from docs: mapbox/mapbox-agent-skills
7Installs
-
AddedFeb 4, 2026

Skill Details

SKILL.md

Integration patterns for Mapbox Maps SDK on iOS with Swift, SwiftUI, UIKit, lifecycle management, and mobile optimization best practices.

Overview

# Mapbox iOS Integration Patterns

Official integration patterns for Mapbox Maps SDK on iOS. Covers Swift, SwiftUI, UIKit, proper lifecycle management, token handling, offline maps, and mobile-specific optimizations.

Use this skill when:

  • Setting up Mapbox Maps SDK for iOS in a new or existing project
  • Integrating maps with SwiftUI or UIKit
  • Implementing proper lifecycle management and cleanup
  • Managing tokens securely in iOS apps
  • Working with offline maps and caching
  • Integrating Navigation SDK
  • Optimizing for battery life and memory usage
  • Debugging crashes, memory leaks, or performance issues

---

Core Integration Patterns

SwiftUI Pattern (iOS 13+)

Modern approach using SwiftUI and Combine

```swift

import SwiftUI

import MapboxMaps

struct MapView: UIViewRepresentable {

@Binding var coordinate: CLLocationCoordinate2D

@Binding var zoom: CGFloat

func makeUIView(context: Context) -> MapboxMap.MapView {

let mapView = MapboxMap.MapView(frame: .zero)

// Configure map

mapView.mapboxMap.setCamera(

to: CameraOptions(

center: coordinate,

zoom: zoom

)

)

return mapView

}

func updateUIView(_ mapView: MapboxMap.MapView, context: Context) {

// Update camera when SwiftUI state changes

mapView.mapboxMap.setCamera(

to: CameraOptions(

center: coordinate,

zoom: zoom

)

)

}

}

// Usage in SwiftUI view

struct ContentView: View {

@State private var coordinate = CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194)

@State private var zoom: CGFloat = 12

var body: some View {

MapView(coordinate: $coordinate, zoom: $zoom)

.edgesIgnoringSafeArea(.all)

}

}

```

Key points:

  • Use UIViewRepresentable to wrap MapView
  • Bind SwiftUI state to map properties
  • Handle updates in updateUIView
  • No manual cleanup needed (SwiftUI handles it)

UIKit Pattern (Classic)

Traditional UIKit integration with proper lifecycle

```swift

import UIKit

import MapboxMaps

class MapViewController: UIViewController {

private var mapView: MapboxMap.MapView!

override func viewDidLoad() {

super.viewDidLoad()

// Initialize map

mapView = MapboxMap.MapView(frame: view.bounds)

mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]

// Configure map

mapView.mapboxMap.setCamera(

to: CameraOptions(

center: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194),

zoom: 12

)

)

view.addSubview(mapView)

// Add map loaded handler

mapView.mapboxMap.onNext(.mapLoaded) { [weak self] _ in

self?.mapDidLoad()

}

}

private func mapDidLoad() {

// Add sources and layers after map loads

addCustomLayers()

}

private func addCustomLayers() {

// Add your custom sources and layers

}

deinit {

// MapView cleanup happens automatically

// No manual cleanup needed with SDK v10+

}

}

```

Key points:

  • Initialize in viewDidLoad()
  • Use weak self in closures to prevent retain cycles
  • Wait for .mapLoaded event before adding layers
  • No manual cleanup needed (SDK v10+ handles it)

---

Token Management

βœ… Recommended: Info.plist Configuration

```xml

MBXAccessToken

$(MAPBOX_ACCESS_TOKEN)

```

Xcode Build Configuration:

  1. Create .xcconfig file:

```bash

# Config/Secrets.xcconfig (add to .gitignore)

MAPBOX_ACCESS_TOKEN = pk.your_token_here

```

  1. Set in Xcode project settings:

- Select project β†’ Info tab

- Add Configuration Set: Secrets.xcconfig

  1. Add to .gitignore:

```gitignore

Config/Secrets.xcconfig

*.xcconfig

```

Why this pattern:

  • Token not in source code
  • Automatically injected at build time
  • Works with Xcode Cloud and CI/CD
  • No hardcoded secrets

❌ Anti-Pattern: Hardcoded Tokens

```swift

// ❌ NEVER DO THIS - Token in source code

MapboxOptions.accessToken = "pk.YOUR_MAPBOX_TOKEN_HERE"

```

---

Memory Management and Lifecycle

βœ… Proper Retain Cycle Prevention

```swift

class MapViewController: UIViewController {

private var mapView: MapboxMap.MapView!

private var cancelables = Set()

override func viewDidLoad() {

super.viewDidLoad()

setupMap()

}

private func setupMap() {

mapView = MapboxMap.MapView(frame: view.bounds)

view.addSubview(mapView)

// βœ… GOOD: Use weak self to prevent retain cycles

mapView.mapboxMap.onEvery(.cameraChanged) { [weak self] event in

self?.handleCameraChange(event)

}

// βœ… GOOD: Store cancelables for proper cleanup

mapView.gestures.onMapTap

.sink { [weak self] coordinate in

self?.handleTap(at: coordinate)

}

.store(in: &cancelables)

}

private func handleCameraChange(_ event: MapboxCoreMaps.Event) {

// Handle camera changes

}

private func handleTap(at coordinate: CLLocationCoordinate2D) {

// Handle tap

}

deinit {

// Cancelables automatically cleaned up

print("MapViewController deallocated")

}

}

```

❌ Anti-Pattern: Retain Cycles

```swift

// ❌ BAD: Strong reference cycle

mapView.mapboxMap.onEvery(.cameraChanged) { event in

self.handleCameraChange(event) // Retains self!

}

// ❌ BAD: Not storing cancelables

mapView.gestures.onMapTap

.sink { coordinate in

self.handleTap(at: coordinate)

}

// Immediately deallocated!

```

---

Offline Maps

Download Region for Offline Use

```swift

import MapboxMaps

class OfflineManager {

private let offlineManager: OfflineRegionManager

init() {

offlineManager = OfflineRegionManager()

}

func downloadRegion(

name: String,

bounds: CoordinateBounds,

minZoom: Double = 0,

maxZoom: Double = 16,

completion: @escaping (Result) -> Void

) {

// Create tile pyramid definition

let tilePyramid = TilePyramidOfflineRegionDefinition(

styleURL: StyleURI.streets.rawValue,

bounds: bounds,

minZoom: minZoom,

maxZoom: maxZoom

)

// Create offline region

offlineManager.createOfflineRegion(

for: tilePyramid,

metadata: ["name": name]

) { result in

switch result {

case .success(let region):

// Download tiles

region.setOfflineRegionDownloadState(to: .active)

// Monitor progress

region.observeOfflineRegionDownloadStatus { status in

print("Downloaded: \(status.completedResourceCount)/\(status.requiredResourceCount)")

}

completion(.success(()))

case .failure(let error):

completion(.failure(error))

}

}

}

func listOfflineRegions() -> [OfflineRegion] {

return offlineManager.offlineRegions

}

func deleteRegion(_ region: OfflineRegion, completion: @escaping (Result) -> Void) {

offlineManager.removeOfflineRegion(for: region) { result in

completion(result.map { _ in () })

}

}

}

```

Key considerations:

  • Battery impact: Downloading uses significant battery
  • Storage limits: Monitor available disk space
  • Zoom levels: Higher zoom = more tiles = more storage
  • Style updates: Offline regions don't auto-update styles

Storage Calculations

```swift

// Estimate offline region size before downloading

func estimateSize(bounds: CoordinateBounds, maxZoom: Double) -> Int64 {

let tilePyramid = TilePyramidOfflineRegionDefinition(

styleURL: StyleURI.streets.rawValue,

bounds: bounds,

minZoom: 0,

maxZoom: maxZoom

)

// Rough estimate: 50 KB per tile average

let tileCount = tilePyramid.tileCount

return tileCount * 50_000 // bytes

}

// Check available storage

func hasEnoughStorage(requiredBytes: Int64) -> Bool {

let fileURL = URL(fileURLWithPath: NSHomeDirectory())

guard let values = try? fileURL.resourceValues(forKeys: [.volumeAvailableCapacityKey]),

let capacity = values.volumeAvailableCapacity else {

return false

}

return Int64(capacity) > requiredBytes * 2 // 2x buffer

}

```

---

Navigation SDK Integration

Basic Navigation Setup

```swift

import MapboxMaps

import MapboxNavigation

import MapboxDirections

import MapboxCoreNavigation

class NavigationViewController: UIViewController {

private var navigationMapView: NavigationMapView!

private var routeController: RouteController?

override func viewDidLoad() {

super.viewDidLoad()

setupNavigationMap()

}

private func setupNavigationMap() {

navigationMapView = NavigationMapView(frame: view.bounds)

navigationMapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]

view.addSubview(navigationMapView)

}

func startNavigation(to destination: CLLocationCoordinate2D) {

guard let origin = navigationMapView.mapView.location.latestLocation?.coordinate else {

return

}

// Request route

let waypoints = [

Waypoint(coordinate: origin),

Waypoint(coordinate: destination)

]

let options = NavigationRouteOptions(waypoints: waypoints)

Directions.shared.calculate(options) { [weak self] session, result in

guard let self = self else { return }

switch result {

case .success(let response):

guard let route = response.routes?.first else { return }

// Show route on map

self.navigationMapView.show([route])

self.navigationMapView.showWaypoints(on: route)

// Start navigation

self.startActiveNavigation(with: route)

case .failure(let error):

print("Route calculation failed: \(error)")

}

}

}

private func startActiveNavigation(with route: Route) {

let navigationService = MapboxNavigationService(

route: route,

routeOptions: route.routeOptions,

simulating: .never

)

routeController = RouteController(

navigationService: navigationService

)

// Listen to navigation events

routeController?.delegate = self

}

}

extension NavigationViewController: RouteControllerDelegate {

func routeController(

_ routeController: RouteController,

didUpdate locations: [CLLocation]

) {

// Update user location

}

func routeController(

_ routeController: RouteController,

didArriveAt waypoint: Waypoint

) {

print("Arrived at destination!")

}

}

```

Navigation SDK features:

  • Turn-by-turn guidance
  • Voice instructions
  • Route progress tracking
  • Rerouting
  • Traffic-aware routing
  • Offline navigation (with offline regions)

---

Mobile Performance Optimization

Battery Optimization

```swift

// βœ… Reduce frame rate when app is in background

class BatteryAwareMapViewController: UIViewController {

private var mapView: MapboxMap.MapView!

override func viewDidLoad() {

super.viewDidLoad()

setupMap()

observeAppState()

}

private func observeAppState() {

NotificationCenter.default.addObserver(

self,

selector: #selector(appDidEnterBackground),

name: UIApplication.didEnterBackgroundNotification,

object: nil

)

NotificationCenter.default.addObserver(

self,

selector: #selector(appWillEnterForeground),

name: UIApplication.willEnterForegroundNotification,

object: nil

)

}

@objc private func appDidEnterBackground() {

// Reduce rendering when in background

mapView.mapboxMap.setRenderCacheSize(to: 0)

// Pause expensive operations

mapView.location.options.activityType = .otherNavigation

}

@objc private func appWillEnterForeground() {

// Resume normal rendering

mapView.mapboxMap.setRenderCacheSize(to: nil) // Default

// Resume location updates

mapView.location.options.activityType = .fitness

}

}

```

Memory Optimization

```swift

// βœ… Handle memory warnings

override func didReceiveMemoryWarning() {

super.didReceiveMemoryWarning()

// Clear map cache

mapView?.mapboxMap.clearData { result in

switch result {

case .success:

print("Map cache cleared")

case .failure(let error):

print("Failed to clear cache: \(error)")

}

}

}

// βœ… Limit cached tiles

let resourceOptions = ResourceOptions(

accessToken: accessToken,

tileStoreUsageMode: .readOnly

)

// βœ… Use appropriate map scale for device

if UIScreen.main.scale > 2.0 {

// Retina displays, can use higher detail

} else {

// Lower DPI, reduce detail

}

```

Network Optimization

```swift

// βœ… Detect network conditions and adjust

import Network

class NetworkAwareMapViewController: UIViewController {

private let monitor = NWPathMonitor()

private let queue = DispatchQueue.global(qos: .background)

override func viewDidLoad() {

super.viewDidLoad()

setupNetworkMonitoring()

}

private func setupNetworkMonitoring() {

monitor.pathUpdateHandler = { [weak self] path in

if path.status == .satisfied {

if path.isExpensive {

// Cellular connection - reduce data usage

self?.enableLowDataMode()

} else {

// WiFi - normal quality

self?.enableNormalMode()

}

}

}

monitor.start(queue: queue)

}

private func enableLowDataMode() {

// Use lower resolution tiles on cellular

// Reduce tile prefetching

}

private func enableNormalMode() {

// Use full resolution

}

}

```

---

Common Mistakes and Solutions

❌ Mistake 1: Not Using Weak Self

```swift

// ❌ BAD: Creates retain cycle

mapView.mapboxMap.onNext(.mapLoaded) { _ in

self.setupLayers() // Retains self!

}

// βœ… GOOD: Use weak self

mapView.mapboxMap.onNext(.mapLoaded) { [weak self] _ in

self?.setupLayers()

}

```

❌ Mistake 2: Adding Layers Before Map Loads

```swift

// ❌ BAD: Adding layers immediately

override func viewDidLoad() {

super.viewDidLoad()

mapView = MapboxMap.MapView(frame: view.bounds)

view.addSubview(mapView)

addCustomLayers() // Map not loaded yet!

}

// βœ… GOOD: Wait for map loaded event

override func viewDidLoad() {

super.viewDidLoad()

mapView = MapboxMap.MapView(frame: view.bounds)

view.addSubview(mapView)

mapView.mapboxMap.onNext(.mapLoaded) { [weak self] _ in

self?.addCustomLayers()

}

}

```

❌ Mistake 3: Ignoring Location Permissions

```swift

// ❌ BAD: Enabling location without checking permissions

mapView.location.options.puckType = .puck2D()

// βœ… GOOD: Request and check permissions

import CoreLocation

class MapViewController: UIViewController, CLLocationManagerDelegate {

private let locationManager = CLLocationManager()

override func viewDidLoad() {

super.viewDidLoad()

setupLocation()

}

private func setupLocation() {

locationManager.delegate = self

switch locationManager.authorizationStatus {

case .notDetermined:

locationManager.requestWhenInUseAuthorization()

case .authorizedWhenInUse, .authorizedAlways:

enableLocationTracking()

default:

// Handle denied/restricted

break

}

}

private func enableLocationTracking() {

mapView.location.options.puckType = .puck2D()

}

func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {

if manager.authorizationStatus == .authorizedWhenInUse ||

manager.authorizationStatus == .authorizedAlways {

enableLocationTracking()

}

}

}

```

Add to Info.plist:

```xml

NSLocationWhenInUseUsageDescription

We need your location to show you on the map

```

❌ Mistake 4: Not Handling Camera State in SwiftUI

```swift

// ❌ BAD: No way to read camera state changes

struct MapView: UIViewRepresentable {

@Binding var coordinate: CLLocationCoordinate2D

func makeUIView(context: Context) -> MapboxMap.MapView {

let mapView = MapboxMap.MapView(frame: .zero)

// User pans map, but coordinate binding never updates!

return mapView

}

}

// βœ… GOOD: Use Coordinator to sync camera state

struct MapView: UIViewRepresentable {

@Binding var coordinate: CLLocationCoordinate2D

@Binding var zoom: CGFloat

func makeCoordinator() -> Coordinator {

Coordinator(coordinate: $coordinate, zoom: $zoom)

}

func makeUIView(context: Context) -> MapboxMap.MapView {

let mapView = MapboxMap.MapView(frame: .zero)

// Listen to camera changes

mapView.mapboxMap.onEvery(.cameraChanged) { _ in

context.coordinator.updateFromMap(mapView.mapboxMap)

}

return mapView

}

class Coordinator {

@Binding var coordinate: CLLocationCoordinate2D

@Binding var zoom: CGFloat

init(coordinate: Binding, zoom: Binding) {

_coordinate = coordinate

_zoom = zoom

}

func updateFromMap(_ map: MapboxMap) {

coordinate = map.cameraState.center

zoom = CGFloat(map.cameraState.zoom)

}

}

}

```

---

Testing Patterns

Unit Testing Map Logic

```swift

import XCTest

@testable import YourApp

import MapboxMaps

class MapLogicTests: XCTestCase {

func testCoordinateConversion() {

let coordinate = CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194)

// Test your map logic without creating actual MapView

let converted = YourMapLogic.convert(coordinate: coordinate)

XCTAssertEqual(converted.latitude, 37.7749, accuracy: 0.001)

}

}

```

UI Testing with Maps

```swift

import XCTest

class MapUITests: XCTestCase {

func testMapViewLoads() {

let app = XCUIApplication()

app.launch()

// Wait for map to load

let mapView = app.otherElements["mapView"]

XCTAssertTrue(mapView.waitForExistence(timeout: 5))

}

}

```

Set accessibility identifier:

```swift

mapView.accessibilityIdentifier = "mapView"

```

---

Troubleshooting

Map Not Displaying

Checklist:

  1. βœ… Token configured in Info.plist?
  2. βœ… Bundle ID matches token restrictions?
  3. βœ… MapboxMaps framework imported?
  4. βœ… MapView added to view hierarchy?
  5. βœ… Internet connection available? (for non-cached tiles)

Memory Leaks

Use Instruments:

  1. Xcode β†’ Product β†’ Profile β†’ Leaks
  2. Look for retain cycles in map event handlers
  3. Ensure [weak self] in all closures
  4. Check that cancelables are stored and cleaned up

Slow Performance

Common causes:

  • Too many markers (use clustering or symbols)
  • Large GeoJSON sources (use vector tiles)
  • High-frequency camera updates
  • Not handling memory warnings
  • Running on simulator (use device for accurate testing)

---

Platform-Specific Considerations

iOS Version Support

  • iOS 13+: Full SwiftUI support
  • iOS 12: UIKit only
  • iOS 11: Limited features

Device Optimization

```swift

// Adjust quality based on device

if UIDevice.current.userInterfaceIdiom == .pad {

// iPad - can handle higher detail

} else if ProcessInfo.processInfo.isLowPowerModeEnabled {

// iPhone in low power mode - reduce detail

}

```

Screen Resolution

```swift

let scale = UIScreen.main.scale

if scale >= 3.0 {

// @3x displays (iPhone Pro models)

// Use highest quality

} else if scale >= 2.0 {

// @2x displays

// Standard quality

}

```

---

Reference

  • [Mapbox Maps SDK for iOS](https://docs.mapbox.com/ios/maps/guides/)
  • [API Reference](https://docs.mapbox.com/ios/maps/api-reference/)
  • [Examples](https://docs.mapbox.com/ios/maps/examples/)
  • [Navigation SDK](https://docs.mapbox.com/ios/navigation/guides/)
  • [Swift Package Manager Installation](https://docs.mapbox.com/ios/maps/guides/install/)
  • [Migration Guides](https://docs.mapbox.com/ios/maps/guides/migrate-to-v10/)

More from this repository8

🎯
mapbox-web-performance-patterns🎯Skill

Optimizes Mapbox web applications by eliminating initialization waterfalls, reducing load times, and improving rendering performance across data loading and map rendering.

🎯
mapbox-web-integration-patterns🎯Skill

Provides official, production-ready integration patterns for embedding Mapbox GL JS across web frameworks with best practices and setup guidance.

🎯
mapbox-style-patterns🎯Skill

Provides curated Mapbox style patterns and layer configurations for implementing common mapping use cases like restaurant finders, real estate, and data visualization.

🎯
mapbox-style-quality🎯Skill

Validates and optimizes Mapbox styles by checking expressions, accessibility, data integrity, and performance before production deployment.

🎯
mapbox-token-security🎯Skill

Secures Mapbox access tokens by implementing granular scope management, URL restrictions, and token rotation strategies across different environments.

🎯
mapbox-cartography🎯Skill

Guides map designers in creating visually compelling and accessible Mapbox maps using expert cartographic principles, color theory, and typography best practices.

🎯
mapbox-android-patterns🎯Skill

Provides robust, lifecycle-aware Mapbox Maps SDK integration patterns for Android using Kotlin and Jetpack Compose.

🎯
mapbox-google-maps-migration🎯Skill

Helps developers seamlessly migrate from Google Maps Platform to Mapbox GL JS by providing comprehensive API translation, styling patterns, and migration strategies.