Skip to main content

Debug map

This example shows how the map looks with different debug options.
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?