Skip to main content

Debug map

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 and creates UI that shows performance statistics and tile data, while updating data by pulling from the MapViewDebugOptions and 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.

iOS Examples App Available

This example code is part of the Maps SDK for iOS Examples App, a working iOS project available on Github. 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 tutorial for step-by-step instructions.

DebugMapExample.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: .performance(.init([.perFrame, .cumulative], samplingDurationMillis: 5000)), title: "Performance statistics"),
]

override func viewDidLoad() {
super.viewDidLoad()

mapView = MapView(frame: view.bounds)
if #available(iOS 15.0, *) {
let maxFPS = Float(UIScreen.main.maximumFramesPerSecond)
mapView.preferredFrameRateRange = CAFrameRateRange(minimum: 1, maximum: maxFPS, preferred: maxFPS)
}

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 tileCover = UIBarButtonItem(
title: "Tiles",
style: .plain,
target: self,
action: #selector(tileCover))
navigationItem.rightBarButtonItems = [debugOptionsBarItem, tileCover]
}



@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)
}

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

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

extension ViewController: DebugOptionSettingsDelegate {
func settingsDidChange(debugOptions: MapViewDebugOptions, performanceOptions: PerformanceStatisticsOptions?) {
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 debugOptions = settings
.filter(\.isEnabled)
.compactMap(\.option.debugOption)
.reduce(MapViewDebugOptions()) { result, next in result.union(next) }

let performanceOptions = settings
.filter(\.isEnabled)
.compactMap(\.option.performanceOption)

delegate?.settingsDidChange(debugOptions: debugOptions, performanceOptions: performanceOptions.first)
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?)
}

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

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

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).
"""
}
}
Was this example helpful?