mapbox-ios-patterns
π―Skillfrom mapbox/mapbox-agent-skills
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)
Installation
npx add-skill mapbox/mapbox-agent-skillsnpx add-skill mapbox/mapbox-agent-skills --skill mapbox-web-performance-patternsnpx add-skill mapbox/mapbox-agent-skills --listnpx add-skill mapbox/mapbox-agent-skills -a cursornpx add-skill mapbox/mapbox-agent-skills -a vscode+ 2 more commands
Skill Details
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
UIViewRepresentableto 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 selfin closures to prevent retain cycles - Wait for
.mapLoadedevent before adding layers - No manual cleanup needed (SDK v10+ handles it)
---
Token Management
β Recommended: Info.plist Configuration
```xml
```
Xcode Build Configuration:
- Create
.xcconfigfile:
```bash
# Config/Secrets.xcconfig (add to .gitignore)
MAPBOX_ACCESS_TOKEN = pk.your_token_here
```
- Set in Xcode project settings:
- Select project β Info tab
- Add Configuration Set: Secrets.xcconfig
- 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
) {
// 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
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
```
β 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
_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:
- β Token configured in Info.plist?
- β Bundle ID matches token restrictions?
- β MapboxMaps framework imported?
- β MapView added to view hierarchy?
- β Internet connection available? (for non-cached tiles)
Memory Leaks
Use Instruments:
- Xcode β Product β Profile β Leaks
- Look for retain cycles in map event handlers
- Ensure
[weak self]in all closures - 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
Optimizes Mapbox web applications by eliminating initialization waterfalls, reducing load times, and improving rendering performance across data loading and map rendering.
Provides official, production-ready integration patterns for embedding Mapbox GL JS across web frameworks with best practices and setup guidance.
Provides curated Mapbox style patterns and layer configurations for implementing common mapping use cases like restaurant finders, real estate, and data visualization.
Validates and optimizes Mapbox styles by checking expressions, accessibility, data integrity, and performance before production deployment.
Secures Mapbox access tokens by implementing granular scope management, URL restrictions, and token rotation strategies across different environments.
Guides map designers in creating visually compelling and accessible Mapbox maps using expert cartographic principles, color theory, and typography best practices.
Provides robust, lifecycle-aware Mapbox Maps SDK integration patterns for Android using Kotlin and Jetpack Compose.
Helps developers seamlessly migrate from Google Maps Platform to Mapbox GL JS by providing comprehensive API translation, styling patterns, and migration strategies.