SwiftUI Map Annotations

Hello there, i've came here after searching por a while a solution to the problem i have but i couldn't find anything helpfull.

I'm displaying a map in iOS16 using only swiftUI and i'm wondering if its any way to know if the region of the map it's displaying has any annotations or not. I want to display a list in the bottom of the map of annotations that are visible in that map area, but if the user drag to another position without annotations i want to hide that list.

Answered by BabyJ in 765113022

Since you already have the visible region shown on the map and all annotations, with their coordinates, you can work out which annotations are located in that region.

MKMapRect has a useful method contains(_:) which indicates whether the specified map point lies within the rectangle. There are two (small?) problems though:

  1. Map expects annotations to have location coordinates of type CLLocationCoordinate2D but MKMapRect uses MKMapPoint. Luckily, an MKMapPoint can be formed from a CLLocationCoordinate2D.
  2. I'm assuming you are storing your map's region as an MKCoordinationRegion object inside of an @State variable. If so, this needs to be converted to an MKMapRect object and that isn't an easy process. If you don't require using MKCoordinationRegion, then store an MKMapRect instead which will make things easier. If you do need it, you will need to manually convert (a quick search online will yield some results), and don't forget about the 180th meridian.

[Note: iOS 17 doesn't make Problem 2 an issue, with the new APIs handling things differently.]


Here's a demo app I made to showcase how you can achieve this:

// A single map annotation as an object
struct AnnotationItem: Identifiable {
    let id = UUID()
    let title: String
    let coordinates: CLLocationCoordinate2D
}

struct MapAnnotations: View {
    // Store the map's currently visible rect
    @State private var visibleRect = MKMapRect(x: 125_000_000, y: 75_000_000, width: 15_000_000, height: 25_000_000)

    // Dummy data
    let items: [AnnotationItem] = [
        .init(title: "San Francisco", coordinates: .init(latitude: 37.77938, longitude: -122.41843)),
        .init(title: "New York", coordinates: .init(latitude: 40.71298, longitude: -74.00720)),
        .init(title: "São Paulo", coordinates: .init(latitude: -23.57964, longitude: -46.65506)),
        .init(title: "London", coordinates: .init(latitude: 51.50335, longitude: -0.07940)),
        .init(title: "Rome", coordinates: .init(latitude: 41.88929, longitude: 12.49355)),
        .init(title: "Johannesburg", coordinates: .init(latitude: -26.20227, longitude: 28.04363)),
        .init(title: "Mumbai", coordinates: .init(latitude: 18.94010, longitude: 72.83466)),
        .init(title: "Tokyo", coordinates: .init(latitude: 35.68951, longitude: 139.69170)),
        .init(title: "Melbourne", coordinates: .init(latitude: -37.81503, longitude: 144.96634))
    ]

    var body: some View {
        Map(mapRect: $visibleRect, annotationItems: items) { item in
            MapMarker(coordinate: item.coordinates)
        }
        .ignoresSafeArea()
        .overlay(alignment: .bottom) {
            let annotations = visibleAnnotations()

            // Show the list of visible annotations if there are any
            if !annotations.isEmpty {
                VStack {
                    ForEach(annotations) { item in
                        Text(item.title)
                            .font(.title3.bold())
                    }
                }
                .padding(10)
                .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 10))
                .padding()
            }
        }
    }

    func visibleAnnotations() -> [AnnotationItem] {
        items.filter { item in
            // Check if the item's location is in the map's currently visible rect
            visibleRect.contains(.init(item.coordinates))
        }
    }
}


Any questions about this please let me know and I'll be happy to answer.

Accepted Answer

Since you already have the visible region shown on the map and all annotations, with their coordinates, you can work out which annotations are located in that region.

MKMapRect has a useful method contains(_:) which indicates whether the specified map point lies within the rectangle. There are two (small?) problems though:

  1. Map expects annotations to have location coordinates of type CLLocationCoordinate2D but MKMapRect uses MKMapPoint. Luckily, an MKMapPoint can be formed from a CLLocationCoordinate2D.
  2. I'm assuming you are storing your map's region as an MKCoordinationRegion object inside of an @State variable. If so, this needs to be converted to an MKMapRect object and that isn't an easy process. If you don't require using MKCoordinationRegion, then store an MKMapRect instead which will make things easier. If you do need it, you will need to manually convert (a quick search online will yield some results), and don't forget about the 180th meridian.

[Note: iOS 17 doesn't make Problem 2 an issue, with the new APIs handling things differently.]


Here's a demo app I made to showcase how you can achieve this:

// A single map annotation as an object
struct AnnotationItem: Identifiable {
    let id = UUID()
    let title: String
    let coordinates: CLLocationCoordinate2D
}

struct MapAnnotations: View {
    // Store the map's currently visible rect
    @State private var visibleRect = MKMapRect(x: 125_000_000, y: 75_000_000, width: 15_000_000, height: 25_000_000)

    // Dummy data
    let items: [AnnotationItem] = [
        .init(title: "San Francisco", coordinates: .init(latitude: 37.77938, longitude: -122.41843)),
        .init(title: "New York", coordinates: .init(latitude: 40.71298, longitude: -74.00720)),
        .init(title: "São Paulo", coordinates: .init(latitude: -23.57964, longitude: -46.65506)),
        .init(title: "London", coordinates: .init(latitude: 51.50335, longitude: -0.07940)),
        .init(title: "Rome", coordinates: .init(latitude: 41.88929, longitude: 12.49355)),
        .init(title: "Johannesburg", coordinates: .init(latitude: -26.20227, longitude: 28.04363)),
        .init(title: "Mumbai", coordinates: .init(latitude: 18.94010, longitude: 72.83466)),
        .init(title: "Tokyo", coordinates: .init(latitude: 35.68951, longitude: 139.69170)),
        .init(title: "Melbourne", coordinates: .init(latitude: -37.81503, longitude: 144.96634))
    ]

    var body: some View {
        Map(mapRect: $visibleRect, annotationItems: items) { item in
            MapMarker(coordinate: item.coordinates)
        }
        .ignoresSafeArea()
        .overlay(alignment: .bottom) {
            let annotations = visibleAnnotations()

            // Show the list of visible annotations if there are any
            if !annotations.isEmpty {
                VStack {
                    ForEach(annotations) { item in
                        Text(item.title)
                            .font(.title3.bold())
                    }
                }
                .padding(10)
                .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 10))
                .padding()
            }
        }
    }

    func visibleAnnotations() -> [AnnotationItem] {
        items.filter { item in
            // Check if the item's location is in the map's currently visible rect
            visibleRect.contains(.init(item.coordinates))
        }
    }
}


Any questions about this please let me know and I'll be happy to answer.

SwiftUI Map Annotations
 
 
Q