---
name: mapbox-ios-patterns
description: Integration patterns for Mapbox Maps SDK on iOS with Swift, SwiftUI, UIKit, lifecycle management, and mobile optimization best practices.
---
# 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
```
2. Set in Xcode project settings:
- Select project → Info tab
- Add Configuration Set: Secrets.xcconfig
3. 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/)