mapbox-ios-patterns

安装量: 153
排名: #5616

安装

npx skills add https://github.com/mapbox/mapbox-agent-skills --skill mapbox-ios-patterns

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 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 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

< key

MBXAccessToken </ key

< string

$(MAPBOX_ACCESS_TOKEN) </ string

Xcode Build Configuration: Create .xcconfig file:

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 : 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 // ❌ NEVER DO THIS - Token in source code MapboxOptions . accessToken = "pk.YOUR_MAPBOX_TOKEN_HERE" Memory Management and Lifecycle ✅ Proper Retain Cycle Prevention class MapViewController : UIViewController { private var mapView : MapboxMap . MapView ! private var cancelables = Set < AnyCancelable

( ) 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 // ❌ 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 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 , Error

) -> 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 , Error

) -> 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 // 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 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 // ✅ 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
// ✅ 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
// ✅ 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
// ❌ 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
// ❌ 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
// ❌ 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:
<
key
>
NSLocationWhenInUseUsageDescription
</
key
>
<
string
>
We need your location to show you on the map
</
string
>
❌ Mistake 4: Not Handling Camera State in SwiftUI
// ❌ 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
<
CLLocationCoordinate2D
>
,
zoom
:
Binding
<
CGFloat
>
)
{
_coordinate
=
coordinate
_zoom
=
zoom
}
func
updateFromMap
(
_
map
:
MapboxMap
)
{
coordinate
=
map
.
cameraState
.
center
zoom
=
CGFloat
(
map
.
cameraState
.
zoom
)
}
}
}
Testing Patterns
Unit Testing Map Logic
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
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:
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 // 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 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 API Reference Examples Navigation SDK Swift Package Manager Installation Migration Guides

返回排行榜