mapbox-ios-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Mapbox 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
    UIViewRepresentable
    to wrap MapView
  • 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)
    }
}
核心要点:
  • 使用
    UIViewRepresentable
    封装MapView
  • 将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
    weak self
    in closures to prevent retain cycles
  • Wait for
    .mapLoaded
    event before adding layers
  • 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:
  1. Create
    .xcconfig
    file:
bash
undefined
xml
<!-- Info.plist -->
<key>MBXAccessToken</key>
<string>$(MAPBOX_ACCESS_TOKEN)</string>
Xcode 构建配置:
  1. 创建
    .xcconfig
    文件:
bash
undefined

Config/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
*.xcconfig
Why 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:
  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)
检查清单:
  1. ✅ Info.plist中是否配置了Token?
  2. ✅ Bundle ID是否与Token限制匹配?
  3. ✅ 是否导入了MapboxMaps框架?
  4. ✅ MapView是否已添加到视图层级?
  5. ✅ 是否有网络连接?(非缓存瓦片需要网络)

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
使用Instruments工具:
  1. Xcode → Product → Profile → Leaks
  2. 查找地图事件处理器中的循环引用
  3. 确保所有闭包中使用
    [weak self]
  4. 检查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
}

Reference

参考资料