mapbox-ios-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMapbox iOS Integration Patterns
Mapbox iOS 集成模式
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
Mapbox Maps SDK在iOS平台的官方集成模式,涵盖Swift、SwiftUI、UIKit、规范的生命周期管理、Token处理、离线地图及移动平台专属优化。
适用场景:
- 在新工程或现有项目中配置Mapbox Maps SDK for iOS
- 集成地图与SwiftUI或UIKit
- 实现规范的生命周期管理与资源清理
- 在iOS应用中安全管理Token
- 离线地图与缓存相关开发
- 集成Navigation SDK
- 电池续航与内存占用优化
- 崩溃、内存泄漏或性能问题调试
Core Integration Patterns
核心集成模式
SwiftUI Pattern (iOS 13+)
SwiftUI 模式(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 to wrap MapView
UIViewRepresentable - Bind SwiftUI state to map properties
- Handle updates in
updateUIView - No manual cleanup needed (SwiftUI handles it)
采用SwiftUI和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)
}
}核心要点:
- 使用封装MapView
UIViewRepresentable - 将SwiftUI状态与地图属性绑定
- 在中处理更新
updateUIView - 无需手动清理(SwiftUI自动处理)
UIKit Pattern (Classic)
UIKit 模式(经典方案)
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 in closures to prevent retain cycles
weak self - Wait for event before adding layers
.mapLoaded - No manual cleanup needed (SDK v10+ handles it)
遵循生命周期规范的传统UIKit集成
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+
}
}核心要点:
- 在中初始化
viewDidLoad() - 在闭包中使用避免循环引用
weak self - 等待事件后再添加图层
.mapLoaded - 无需手动清理(SDK v10+自动处理)
Token Management
Token 管理
✅ Recommended: Info.plist Configuration
✅ 推荐方案:Info.plist 配置
xml
<!-- Info.plist -->
<key>MBXAccessToken</key>
<string>$(MAPBOX_ACCESS_TOKEN)</string>Xcode Build Configuration:
- Create file:
.xcconfig
bash
undefinedxml
<!-- Info.plist -->
<key>MBXAccessToken</key>
<string>$(MAPBOX_ACCESS_TOKEN)</string>Xcode 构建配置:
- 创建文件:
.xcconfig
bash
undefinedConfig/Secrets.xcconfig (add to .gitignore)
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
*.xcconfigWhy this pattern:
- Token not in source code
- Automatically injected at build time
- Works with Xcode Cloud and CI/CD
- No hardcoded secrets
MAPBOX_ACCESS_TOKEN = pk.your_token_here
2. 在Xcode项目设置中配置:
- 选择项目 → Info标签页
- 添加配置集:Secrets.xcconfig
3. 添加到`.gitignore`:
```gitignore
Config/Secrets.xcconfig
*.xcconfig该方案优势:
- Token不暴露在源代码中
- 构建时自动注入
- 兼容Xcode Cloud与CI/CD流程
- 无硬编码密钥
❌ Anti-Pattern: Hardcoded Tokens
❌ 反模式:硬编码Token
swift
// ❌ NEVER DO THIS - Token in source code
MapboxOptions.accessToken = "pk.YOUR_MAPBOX_TOKEN_HERE"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<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")
}
}swift
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
❌ 反模式:循环引用
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!swift
// ❌ BAD: Creates retain 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, 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
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, 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 () })
}
}
}关键注意事项:
- 电池影响: 下载操作会消耗大量电量
- 存储限制: 监控可用磁盘空间
- 缩放级别: 更高缩放级别意味着更多瓦片与存储占用
- 样式更新: 离线区域不会自动更新地图样式
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
}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
Navigation SDK 集成
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)
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 功能:
- 逐向导航指引
- 语音播报
- 路线进度追踪
- 重新规划路线
- 路况感知路由
- 离线导航(需配合离线区域)
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
}
}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
}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
}
}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
❌ 错误1:未使用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()
}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
❌ 错误2:地图加载前添加图层
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()
}
}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
❌ 错误3:忽略位置权限
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
<key>NSLocationWhenInUseUsageDescription</key>
<string>We need your location to show you on the map</string>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()
}
}
}添加至Info.plist:
xml
<key>NSLocationWhenInUseUsageDescription</key>
<string>We need your location to show you on the map</string>❌ Mistake 4: Not Handling Camera State in SwiftUI
❌ 错误4: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<CLLocationCoordinate2D>, zoom: Binding<CGFloat>) {
_coordinate = coordinate
_zoom = zoom
}
func updateFromMap(_ map: MapboxMap) {
coordinate = map.cameraState.center
zoom = CGFloat(map.cameraState.zoom)
}
}
}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<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
地图逻辑单元测试
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)
}
}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
地图UI测试
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"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))
}
}设置可访问性标识符:
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)
检查清单:
- ✅ Info.plist中是否配置了Token?
- ✅ Bundle ID是否与Token限制匹配?
- ✅ 是否导入了MapboxMaps框架?
- ✅ MapView是否已添加到视图层级?
- ✅ 是否有网络连接?(非缓存瓦片需要网络)
Memory Leaks
内存泄漏
Use Instruments:
- Xcode → Product → Profile → Leaks
- Look for retain cycles in map event handlers
- Ensure in all closures
[weak self] - Check that cancelables are stored and cleaned up
使用Instruments工具:
- Xcode → Product → Profile → Leaks
- 查找地图事件处理器中的循环引用
- 确保所有闭包中使用
[weak self] - 检查Cancelables是否已存储并正确清理
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)
常见原因:
- 标记点过多(使用聚类或符号层)
- GeoJSON源过大(使用矢量瓦片)
- 相机更新频率过高
- 未处理内存警告
- 在模拟器运行(使用真机测试获取准确结果)
Platform-Specific Considerations
平台专属注意事项
iOS Version Support
iOS版本支持
- iOS 13+: Full SwiftUI support
- iOS 12: UIKit only
- iOS 11: Limited features
- iOS 13+:完整支持SwiftUI
- iOS 12:仅支持UIKit
- iOS 11:功能受限
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
}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
}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
}