> For the complete documentation index, see [llms.txt](https://docs.mapbox.com/ios/ja/maps/llms.txt)

# Debug map

![This example shows how the map looks with different debug options.](https://docs.mapbox.com/ios/ja/assets/ideal-img/maps-examples-debug-map.ccd6e58.480.png)

This example demonstrates how to display various debug functionalities and performance statistics options using the **Mapbox Maps SDK for iOS**. The code below sets up the [`MapView`](https://docs.mapbox.com/ios/maps/api/latest/documentation/mapboxmaps/mapview/) and creates UI that shows performance statistics and tile data, while updating data by pulling from the [`MapViewDebugOptions`](https://docs.mapbox.com/ios/maps/api/latest/documentation/mapboxmaps/mapviewdebugoptions/) and [`PerformanceStatisticsOptions`](https://docs.mapbox.com/ios/maps/api/latest/documentation/mapboxmaps/performancestatisticsoptions/) to check if their data has updated or not. The `SettingsViewController` class allows users to toggle debug options and save settings, while the `DebugOptionCell` class provides the UI for toggling settings. Performance statistics are handled via the `PerformanceStatistics` extension methods.

> **Note: iOS Examples App Available**
> 
> This example code is part of the **Maps SDK for iOS Examples App**, a working iOS project [available on Github](https://github.com/mapbox/mapbox-maps-ios/tree/11.29.0/Sources/Examples). iOS developers are encouraged to run the examples app locally to interact with this example in an emulator and explore other features of the Maps SDK.
> 
> See our [Run the Maps SDK for iOS Examples App](https://docs.mapbox.com/help/tutorials/maps-sdk-ios-examples-app/) tutorial for step-by-step instructions.

**Swift**

Title: `DebugMapExample.swift`

[View on GitHub](https://github.com/mapbox/mapbox-maps-ios/blob/main/Sources/Examples/All%20Examples/DebugMapExample.swift)

```swift
import UIKit
@_spi(Experimental) import MapboxMaps

final class ViewController: UIViewController {
    private var collectStatisticsButton = UIButton(type: .system)
    private var mapView: MapView!
    private var performanceStatisticsCancelable: AnyCancelable?
    private let settings: [Setting] = [
        Setting(option: .debug(.collision), title: "Debug collision"),
        Setting(option: .debug(.depthBuffer), title: "Show depth buffer"),
        Setting(option: .debug(.overdraw), title: "Debug overdraw"),
        Setting(option: .debug(.parseStatus), title: "Show tile coordinate"),
        Setting(option: .debug(.stencilClip), title: "Show stencil buffer"),
        Setting(option: .debug(.tileBorders), title: "Debug tile clipping"),
        Setting(option: .debug(.timestamps), title: "Show tile loaded time"),
        Setting(option: .debug(.modelBounds), title: "Show 3D model bounding boxes"),
        Setting(option: .debug(.light), title: "Show light conditions"),
        Setting(option: .debug(.camera), title: "Show camera debug view"),
        Setting(option: .debug(.padding), title: "Camera padding"),
        Setting(option: .screenShape, title: "Custom culling shape"),
        Setting(option: .performance(.init([.perFrame, .cumulative], samplingDurationMillis: 5000)), title: "Performance statistics"),
    ]
    private let customCullingShapeLayer: CAShapeLayer = {
        let layer = CAShapeLayer()
        layer.strokeColor = UIColor.white.cgColor
        layer.fillColor = UIColor.clear.cgColor
        layer.lineWidth = 3
        layer.lineJoin = .round
        layer.shadowColor = UIColor.white.cgColor
        layer.shadowRadius = 5
        layer.shadowOpacity = 1
        layer.shouldRasterize = true
        layer.rasterizationScale = UIScreen.main.scale
        return layer
    }()
    private let dimLayer: CAShapeLayer = {
        let layer = CAShapeLayer()
        layer.fillRule = .evenOdd
        layer.fillColor = UIColor.black.withAlphaComponent(0.5).cgColor

        return layer
    }()
    private let customCullingShape = [
        CGPoint(x: 0.35, y: 0.34),  // top-left
        CGPoint(x: 0.65, y: 0.34),  // top-right
        CGPoint(x: 0.85, y: 0.50),  // right
        CGPoint(x: 0.65, y: 0.66),  // bottom-right
        CGPoint(x: 0.35, y: 0.66),  // bottom-left
        CGPoint(x: 0.15, y: 0.50)   // left
    ]

    override func viewDidLoad() {
        super.viewDidLoad()

        mapView = MapView(frame: view.bounds)
        let maxFPS = Float(UIScreen.main.maximumFramesPerSecond)
        mapView.preferredFrameRateRange = CAFrameRateRange(minimum: 1, maximum: maxFPS, preferred: maxFPS)
        mapView.ornaments.options.scaleBar.units = .nautical
        view.addSubview(mapView)
        view.backgroundColor = .skyBlue
        mapView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            mapView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            mapView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
        ])

        let debugOptionsBarItem = UIBarButtonItem(
            title: "Debug",
            style: .plain,
            target: self,
            action: #selector(openDebugOptionsMenu(_:)))
        let infoBarItem = UIBarButtonItem(
            title: "Info",
            style: .plain,
            target: self,
            action: #selector(showInfo))
        navigationItem.rightBarButtonItems = [debugOptionsBarItem, infoBarItem]
    }

    

    @objc private func openDebugOptionsMenu(_ sender: UIBarButtonItem) {
        let settingsViewController = SettingsViewController(settings: settings)
        settingsViewController.delegate = self

        let navigationController = UINavigationController(rootViewController: settingsViewController)
        navigationController.modalPresentationStyle = .popover
        navigationController.popoverPresentationController?.barButtonItem = sender

        present(navigationController, animated: true, completion: nil)
    }

    private func extractStyleInfo() -> StyleInfo {
        let styleJSON = mapView.mapboxMap.styleJSON

        guard let data = styleJSON.data(using: .utf8),
              let parsedStyle = try? JSONDecoder().decode(StyleJson.self, from: data) else {
            return StyleInfo(modifiedDate: "Unknown", sdkCompatibility: "Unknown", styleURL: "Unknown")
        }

        let modifiedDate = parsedStyle.modified ?? "Unknown"

        let sdkCompatibility: String
        if let compatibility = parsedStyle.metadata?.compatibility {
            var compatibilityParts: [String] = []
            if let ios = compatibility.ios {
                compatibilityParts.append("iOS: \(ios)")
            }
            if let android = compatibility.android {
                compatibilityParts.append("Android: \(android)")
            }
            if let js = compatibility.js {
                compatibilityParts.append("JS: \(js)")
            }
            sdkCompatibility = compatibilityParts.isEmpty ? "Unknown" : compatibilityParts.joined(separator: "\n")
        } else {
            sdkCompatibility = "Unknown"
        }

        let styleURL: String
        if let origin = parsedStyle.metadata?.origin, !origin.isEmpty {
            styleURL = origin
        } else {
            styleURL = mapView.mapboxMap.styleURI?.rawValue ?? "Unknown"
        }

        return StyleInfo(modifiedDate: modifiedDate, sdkCompatibility: sdkCompatibility, styleURL: styleURL)
    }

    @objc private func showInfo() {
        // Get tiles information
        let tileIds = mapView.mapboxMap.tileCover(for: TileCoverOptions(tileSize: 512, minZoom: 0, maxZoom: 22, roundZoom: false))
        let tilesMessage = tileIds.map { "\($0.z)/\($0.x)/\($0.y)" }.joined(separator: "\n")

        // Get style information
        let styleInfo = extractStyleInfo()
        let styleMessage = """
        Style URL: \(styleInfo.styleURL)
        Modified: \(styleInfo.modifiedDate)
        SDK Compatibility:
        \(styleInfo.sdkCompatibility)
        """

        // Combine both
        let combinedMessage = """
        TILES:
        \(tilesMessage)

        STYLE INFO:
        \(styleMessage)
        """
        showAlert(withTitle: "Map Info", and: combinedMessage)
    }

    private func handle(statistics: PerformanceStatistics) {
        showAlert(with: "\(statistics.topRenderedGroupDescription)\n\(statistics.renderingDurationStatisticsDescription)")
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        let scaledShape = customCullingShape.map { CGPoint(x: $0.x * mapView.bounds.width, y: $0.y * mapView.bounds.height) }
        let cutoutPath = UIBezierPath()
        cutoutPath.move(to: scaledShape.first!)
        scaledShape.dropFirst().forEach(cutoutPath.addLine)
        cutoutPath.close()

        customCullingShapeLayer.path = cutoutPath.cgPath

        let mapViewPath = UIBezierPath(rect: mapView.bounds)
        mapViewPath.append(cutoutPath)
        mapViewPath.usesEvenOddFillRule = true

        dimLayer.path = mapViewPath.cgPath
    }

    private func setScreenShape() {
        mapView.mapboxMap.screenCullingShape = customCullingShape
        mapView.layer.addSublayer(dimLayer)
        mapView.layer.addSublayer(customCullingShapeLayer)
    }

    private func removeScreenShape() {
        customCullingShapeLayer.removeFromSuperlayer()
        dimLayer.removeFromSuperlayer()
        mapView.mapboxMap.screenCullingShape = []
    }
}

extension ViewController: DebugOptionSettingsDelegate {
    func settingsDidChange(
        debugOptions: MapViewDebugOptions,
        performanceOptions: PerformanceStatisticsOptions?,
        screenShapeEnabled: Bool
    ) {
        if screenShapeEnabled {
            setScreenShape()
        } else {
            removeScreenShape()
        }
        mapView.debugOptions = debugOptions

        guard let performanceOptions else { return performanceStatisticsCancelable = nil }
        performanceStatisticsCancelable?.cancel()
        performanceStatisticsCancelable = mapView.mapboxMap.collectPerformanceStatistics(performanceOptions, callback: handle(statistics:))
    }
}

final class SettingsViewController: UIViewController, UITableViewDataSource {
    weak var delegate: DebugOptionSettingsDelegate?
    private var listView: UITableView!
    private let settings: [Setting]

    fileprivate init(settings: [Setting]) {
        self.settings = settings
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        title = "Debug options"
        listView = UITableView()
        listView.dataSource = self
        listView.register(DebugOptionCell.self, forCellReuseIdentifier: String(describing: DebugOptionCell.self))

        view.addSubview(listView)

        listView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            listView.topAnchor.constraint(equalTo: view.topAnchor),
            listView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            listView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            listView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
        ])

        navigationItem.largeTitleDisplayMode = .never
        navigationItem.rightBarButtonItem = UIBarButtonItem(
            barButtonSystemItem: .save,
            target: self,
            action: #selector(saveSettings(_:)))
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        preferredContentSize = listView.contentSize
    }

    @objc private func saveSettings(_ sender: UIBarButtonItem) {
        let enabledSettings = settings.filter({ $0.isEnabled })
        let debugOptions = enabledSettings
            .compactMap(\.option.debugOption)
            .reduce(MapViewDebugOptions()) { result, next in result.union(next) }

        let performanceOptions = enabledSettings
            .compactMap(\.option.performanceOption)

        let screenShapeEnabled = enabledSettings.contains(where: { $0.option.isScreenShape })

        delegate?.settingsDidChange(
            debugOptions: debugOptions,
            performanceOptions: performanceOptions.first,
            screenShapeEnabled: screenShapeEnabled
        )
        dismiss(animated: true, completion: nil)
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        settings.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cellID = String(describing: DebugOptionCell.self)
        // swiftlint:disable:next force_cast
        let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath) as! DebugOptionCell

        let setting = settings[indexPath.row]
        cell.configure(with: setting.title, isOptionEnabled: setting.isEnabled)
        cell.onToggled(setting.toggle)

        return cell
    }
}

// MARK: Cell

private class DebugOptionCell: UITableViewCell {
    private let titleLabel = UILabel()
    private let toggle = UISwitch()
    private var onToggleHandler: (() -> Void)?

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        toggle.addTarget(self, action: #selector(didToggle(_:)), for: .valueChanged)

        contentView.addSubview(titleLabel)
        contentView.addSubview(toggle)

        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        toggle.translatesAutoresizingMaskIntoConstraints = false

        let constraints: [NSLayoutConstraint] = [
            titleLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16),
            titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
            titleLabel.topAnchor.constraint(greaterThanOrEqualTo: contentView.topAnchor, constant: 8),
            toggle.leftAnchor.constraint(greaterThanOrEqualTo: titleLabel.rightAnchor, constant: 16),
            toggle.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16),
            toggle.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor),
            toggle.topAnchor.constraint(greaterThanOrEqualTo: contentView.topAnchor, constant: 8),
        ]
        NSLayoutConstraint.activate(constraints)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func configure(with title: String, isOptionEnabled: Bool) {
        titleLabel.text = title
        toggle.isOn = isOptionEnabled
    }

    func onToggled(_ handler: @escaping () -> Void) {
        onToggleHandler = handler
    }

    @objc private func didToggle(_ sender: UISwitch) {
        onToggleHandler?()
    }
}

protocol DebugOptionSettingsDelegate: AnyObject {
    func settingsDidChange(
        debugOptions: MapViewDebugOptions,
        performanceOptions: PerformanceStatisticsOptions?,
        screenShapeEnabled: Bool
    )
}

private final class Setting {
    enum Option {
        case debug(MapViewDebugOptions)
        case performance(PerformanceStatisticsOptions)
        case screenShape
    }

    let option: Option
    let title: String
    private(set) var isEnabled: Bool

    init(option: Option, title: String, isEnabled: Bool = false) {
        self.option = option
        self.title = title
        self.isEnabled = isEnabled
    }

    func toggle() { isEnabled.toggle() }
}

extension Setting.Option {
    var debugOption: MapViewDebugOptions? {
        if case let .debug(option) = self { return option } else { return nil }
    }

    var performanceOption: PerformanceStatisticsOptions? {
        if case let .performance(option) = self { return option } else { return nil }
    }

    var isScreenShape: Bool {
        if case .screenShape = self { return true } else { return false }
    }
}

extension PerformanceStatistics {
    fileprivate var topRenderedGroupDescription: String {
        if let topRenderedGroup = perFrameStatistics?.topRenderGroups.first {
            return "Top rendered group: `\(topRenderedGroup.name)` took \(topRenderedGroup.durationMillis)ms."
        } else {
            return "No information about topRenderedLayer."
        }
    }

    fileprivate var renderingDurationStatisticsDescription: String {
        guard let drawCalls = cumulativeStatistics?.drawCalls else { return "Cumulative statistics haven't been collected." }
        return """
        Number of draw calls: \(drawCalls).
        """
    }
}

struct StyleInfo {
    let modifiedDate: String
    let sdkCompatibility: String
    let styleURL: String
}

struct StyleJson: Codable {
    let modified: String?
    let metadata: Metadata?
}

struct Metadata: Codable {
    let origin: String?
    let compatibility: Compatibility?

    enum CodingKeys: String, CodingKey {
        case origin = "mapbox:origin"
        case compatibility = "mapbox:compatibility"
    }
}

struct Compatibility: Codable {
    let ios: String?
    let android: String?
    let js: String?
}
```