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

# Access map features using VoiceOver

![Use VoiceOver to highlight annotations and hear their associated features.](https://docs.mapbox.com/ios/assets/ideal-img/maps-examples-voice-over.27fc7fc.480.png)

This example demonstrates the implementation of iOS [`VoiceOver`](https://developer.apple.com/documentation/accessibility/voiceover/) accessibility features in a mapping application using the **Mapbox Maps SDK for iOS**. The example sets up a [`MapView`](https://docs.mapbox.com/ios/maps/api/latest/documentation/mapboxmaps/mapview/) with custom annotations representing various parks. It includes functionality to manage accessibility elements for these annotations, the user's current location, and route shields. The annotations are created using [`PointAnnotation`](https://docs.mapbox.com/ios/maps/api/latest/documentation/mapboxmaps/pointannotation/) and displayed on the map with custom images.

In this example, the map view's elements are dynamically updated based on user interaction and VoiceOver status. Accessibility labels and frames are managed to provide information about visible annotations, the current location (with a puck icon), and route shields. Interaction with annotations is optimized for VoiceOver users, presenting detailed information such as park names. The implementation also involves handling gesture events and updating accessibility elements.

> **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.31.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: `VoiceOverAccessibilityExample.swift`

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

```swift
import UIKit
import Combine
import CoreLocation
import MapboxMaps

final class ViewController: UIViewController {
    struct MyData {
        var id: Int
        var coordinate: CLLocationCoordinate2D
        var name: String
    }

    let data: [MyData] = [
        MyData(id: 0, coordinate: .init(latitude: 40.727405, longitude: -73.981926), name: "Tomkins Square Park"),
        MyData(id: 1, coordinate: .init(latitude: 40.7308963, longitude: -73.998694), name: "Washington Square Park"),
        MyData(id: 2, coordinate: .init(latitude: 40.715225, longitude: -74.000086), name: "Columbus Park"),
        MyData(id: 3, coordinate: .init(latitude: 40.692813, longitude: -73.976161), name: "Fort Greene Park")]

    var mapView: MapView!
    var pointAnnotationManager: PointAnnotationManager!
    var instructionsLabel: UILabel!

    var currentLocationAccessibilityElement: UIAccessibilityElement? {
        didSet {
            accessibilityElementsDidChange()
        }
    }

    var annotationAccessibilityElements = [UIAccessibilityElement]() {
        didSet {
            accessibilityElementsDidChange()
        }
    }

    var routeShieldAccessibilityElements = [UIAccessibilityElement]() {
        didSet {
            accessibilityElementsDidChange()
        }
    }

    private var cancelables = Set<AnyCancelable>()

    override func viewDidLoad() {
        super.viewDidLoad()
        let centerCoordinate = CLLocationCoordinate2D(latitude: 40.7131854, longitude: -74.0165265)
        let options = MapInitOptions(cameraOptions: CameraOptions(center: centerCoordinate, zoom: 10))
        mapView = MapView(frame: view.frame, mapInitOptions: options)
        mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        view.addSubview(mapView)

        mapView.isAccessibilityElement = false
        mapView.accessibilityElements = []

        let location = Location(coordinate: centerCoordinate)
        mapView.location.dataModel = LocationDataModel(location: Just([location]).eraseToAnyPublisher())
        mapView.location.options.puckType = .puck2D(.makeDefault())

        // create point annotation manager to house point annotations
        pointAnnotationManager = mapView.annotations.makePointAnnotationManager()
        pointAnnotationManager.annotations = data.map { dataElement in
            var annotation = PointAnnotation(id: dataElement.id.description, coordinate: dataElement.coordinate)
            annotation.image = .init(image: UIImage(named: "dest-pin")!, name: "custom_marker")
            annotation.customData = ["name": .string(dataElement.name)]
            annotation.iconOffset = [0, 12]
            return annotation
        }

        // configure example instructions label
        instructionsLabel = UILabel()
        instructionsLabel.backgroundColor = .lightGray
        instructionsLabel.textColor = .black
        instructionsLabel.text = "Turn on VoiceOver to interact with the annotations."
        instructionsLabel.textAlignment = .center
        instructionsLabel.lineBreakMode = .byWordWrapping
        instructionsLabel.numberOfLines = 0
        instructionsLabel.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(instructionsLabel)
        NSLayoutConstraint.activate([
            instructionsLabel.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
            instructionsLabel.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),
            instructionsLabel.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor)])
        instructionsLabel.isHidden = UIAccessibility.isVoiceOverRunning
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(voiceOverStatusDidChange),
            name: UIAccessibility.voiceOverStatusDidChangeNotification,
            object: nil)

        // Observe events that require recomputing accessibility elements
        mapView.mapboxMap.onMapLoaded.observeNext { [weak self] _ in
            self?.updateAllAccessibilityElements {
                
            }
        }.store(in: &cancelables)
        mapView.gestures.delegate = self
        mapView.location.onLocationChange.observe { [weak self] location in
            guard let self, let location = location.last else { return }
            self.updateLocationAccessibilityElement(location: location)
        }.store(in: &cancelables)
    }

    @objc private func voiceOverStatusDidChange() {
        instructionsLabel.isHidden = UIAccessibility.isVoiceOverRunning
    }

    func accessibilityElementsDidChange() {
        let summaryAccessibilityElement = UIAccessibilityElement(accessibilityContainer: mapView!)
        summaryAccessibilityElement.accessibilityIdentifier = "map-view-summary"
        summaryAccessibilityElement.accessibilityFrame = UIAccessibility.convertToScreenCoordinates(mapView.bounds, in: mapView)

        switch annotationAccessibilityElements.count {
        case 0:
            summaryAccessibilityElement.accessibilityLabel = "Map view selected. There are 0 visible annotations."
        case 1:
            summaryAccessibilityElement.accessibilityLabel = "Map view selected. There is 1 visible annotation: \(annotationAccessibilityElements.first!.accessibilityLabel!)."
        default:
            summaryAccessibilityElement.accessibilityLabel = "Map view selected. There are \(annotationAccessibilityElements.count) visible annotations: \(annotationAccessibilityElements.compactMap { $0.accessibilityLabel }.joined(separator: ", "))."
        }

        var allAccessibilityElements = [summaryAccessibilityElement]
        if let currentLocationAccessibilityElement = currentLocationAccessibilityElement {
            allAccessibilityElements.append(currentLocationAccessibilityElement)
        }
        allAccessibilityElements.append(contentsOf: annotationAccessibilityElements)
        allAccessibilityElements.append(contentsOf: routeShieldAccessibilityElements)

        mapView.accessibilityElements = allAccessibilityElements
    }

    func updateLocationAccessibilityElement(location: Location) {
        if let accessibilityFrame = mapView.accessibilityFrame(for: location.coordinate) {
            let element = UIAccessibilityElement(accessibilityContainer: mapView!)
            element.accessibilityIdentifier = "puck"
            element.accessibilityLabel = "Current Location"
            element.accessibilityFrame = accessibilityFrame
            currentLocationAccessibilityElement = element
        } else {
            currentLocationAccessibilityElement = nil
        }
    }

    func updateAllAccessibilityElements(completion: @escaping () -> Void = {}) {
        let group = DispatchGroup()

        // update accessibility elements for annotations
        group.enter()
        let pointAnnotationsQueryOptions = RenderedQueryOptions(
            layerIds: [pointAnnotationManager.layerId],
            filter: nil)
        mapView.mapboxMap.queryRenderedFeatures(
            with: mapView.safeAreaLayoutGuide.layoutFrame,
            options: pointAnnotationsQueryOptions) { [weak self] result in
                guard let self = self, let mapView = self.mapView else { return }
                switch result {
                case .success(let queriedFeatures):
                    self.annotationAccessibilityElements = queriedFeatures.compactMap { queriedFeature -> UIAccessibilityElement? in
                        guard case .point(let point) = queriedFeature.queriedFeature.feature.geometry,
                              let accessibilityFrame = mapView.accessibilityFrame(for: point.coordinates),
                              let properties = queriedFeature.queriedFeature.feature.properties?.turfRawValue as? [String: Any],
                              let customData = properties["custom_data"] as? [String: Any],
                              let name = customData["name"] as? String else {
                            return nil
                        }
                        let element = UIAccessibilityElement(accessibilityContainer: mapView)
                        element.accessibilityIdentifier = queriedFeature.queriedFeature.feature.identifier?.description
                        element.accessibilityFrame = accessibilityFrame
                        element.accessibilityLabel = name
                        return element
                    }
                case .failure(let error):
                    self.annotationAccessibilityElements = []
                    print(error)
                }
                group.leave()
            }

        // update accessibility elements for route shields
        group.enter()
        let routeShieldsQueryOptions = RenderedQueryOptions(
            layerIds: ["road-number-shield"],
            filter: Exp(.eq) {
                Exp(.get) {
                    "shield"
                }
                "us-interstate"
            })
        mapView.mapboxMap.queryRenderedFeatures(
            with: mapView.safeAreaLayoutGuide.layoutFrame,
            options: routeShieldsQueryOptions) { [weak self] result in
                guard let self = self, let mapView = self.mapView else { return }
                switch result {
                case .success(let queriedFeatures):
                    // create the UIAccessibility element for each route shield in the map view.
                    self.routeShieldAccessibilityElements = queriedFeatures.compactMap { queriedFeature -> UIAccessibilityElement? in
                        guard case .point(let point) = queriedFeature.queriedFeature.feature.geometry,
                              let accessibilityFrame = mapView.accessibilityFrame(for: point.coordinates),
                              let properties = queriedFeature.queriedFeature.feature.properties?.turfRawValue as? [String: Any],
                              let shieldNumber = properties["ref"] as? String else {
                            return nil
                        }
                        let element = UIAccessibilityElement(accessibilityContainer: mapView)
                        element.accessibilityIdentifier = "shield-\(shieldNumber)"
                        element.accessibilityLabel = "U.S. interstate \(shieldNumber)"
                        element.accessibilityFrame = accessibilityFrame
                        return element
                    }
                case .failure(let error):
                    self.routeShieldAccessibilityElements = []
                    print(error)
                }
                group.leave()
            }

        group.notify(queue: .main, execute: completion)
    }
}

extension ViewController: GestureManagerDelegate {
    func gestureManager(_ gestureManager: GestureManager, didBegin gestureType: GestureType) {
    }

    func gestureManager(_ gestureManager: GestureManager, didEnd gestureType: GestureType, willAnimate: Bool) {
        if !willAnimate {
            updateAllAccessibilityElements()
        }
    }

    func gestureManager(_ gestureManager: GestureManager, didEndAnimatingFor gestureType: GestureType) {
        updateAllAccessibilityElements()
    }
}

private extension MapView {
    func accessibilityFrame(for coordinate: CLLocationCoordinate2D) -> CGRect? {
        let pointInViewSpace = mapboxMap.point(for: coordinate)
        guard pointInViewSpace != CGPoint(x: -1, y: -1) else {
            return nil
        }
        let rectInViewSpace = CGRect(origin: pointInViewSpace, size: .zero).insetBy(dx: -20, dy: -20)
        return UIAccessibility.convertToScreenCoordinates(rectInViewSpace, in: self)
    }
}

extension FeatureIdentifier: CustomStringConvertible {
    public var description: String {
        switch self {
        case .number(let number):
            return number.description
        case .string(let string):
            return string
        #if USING_TURF_WITH_LIBRARY_EVOLUTION
        @unknown default:
            return String(describing: self)
        #endif
        }
    }
}
```