J. Doe
J. Doe

Reputation: 13043

Overlay of annotation circle in Map - SwiftUI

I want to add a circle with a certain radius in Map with SwiftUI only. I want to be able to show a circular overlay in the Map with e.g. 1 km. This is a picture what I want to achieve:

enter image description here

The problem with the code below, is that the circle has a fixed size on the map:

import SwiftUI
import MapKit

struct ContentView: View {
    static let usersLocation = CLLocationCoordinate2D(latitude: 52.0929779694589, longitude: 5.084964426384347)

    @State private var region = MKCoordinateRegion(
        center: ContentView.usersLocation,
        span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
    )
    let annotations = [CurrentUsersAnnotation()]

    
    var body: some View {
        Map(
            coordinateRegion: $region, annotationItems: annotations) { _ in
                MapAnnotation(coordinate: ContentView.usersLocation) {
                    Circle()
                            .strokeBorder(Color.red, lineWidth: 4)
                            .frame(width: 40, height: 40)
                }
            }
    }
}

struct CurrentUsersAnnotation: Identifiable {
    let id = UUID() // Always unique on map
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Is there a SwiftUI-only fix? I am not looking for UIKit answers, I want to do it fully in SwiftUI.

Upvotes: 1

Views: 2645

Answers (2)

murp
murp

Reputation: 441

In iOS 17, this is now supported. Apple has a more in-depth tutorial on their website https://developer.apple.com/videos/play/wwdc2023/10043/. An example from the video:

Map {
    MapCircle(center: someCLLocationCoordinate2D, radius: 100)
        .foregroundStyle(.cyan.opacity(0.5))
        .mapOverlayLevel(level: .aboveRoads)
}

Upvotes: 1

lorem ipsum
lorem ipsum

Reputation: 29271

Overlays aren't supported but you can easily size your View/Circle by figuring out the ratio between the View and the span

First, since you want kilometers you can calculate the distance between the upper and lower edge (latitude span) of the region.

extension MKCoordinateRegion{
    ///Identify the length of the span in meters north to south
    var spanLatitude: Measurement<UnitLength>{
        let loc1 = CLLocation(latitude: center.latitude - span.latitudeDelta * 0.5, longitude: center.longitude)
        let loc2 = CLLocation(latitude: center.latitude + span.latitudeDelta * 0.5, longitude: center.longitude)
        let metersInLatitude = loc1.distance(from: loc2)
        return Measurement(value: metersInLatitude, unit: UnitLength.meters)
    }
}

using GeometryReader you can get the height of the View/Map, then the ratio. Once you have the size per meter just multiply to get kilometer.

//Size per meter
let ratio = (geo.size.height/region.spanLatitude.value)
//Size per kilometer
let kilometerSize = ratio * 1000

Then use the kilometerSize in the frame.

It is likely not be a perfect kilometer given there are likely inconsistencies between the View and the Map but it is probably pretty close. If you can find something that is a known kilometer you can test it out.

struct ScaledAnnotationView: View {
    static let usersLocation = CLLocationCoordinate2D(latitude: 52.0929779694589, longitude: 5.084964426384347)
    
    @State private var region = MKCoordinateRegion(
        center: ScaledAnnotationView.usersLocation,latitudinalMeters: 1000, longitudinalMeters: 1000
    )
    let annotations = [CurrentUsersAnnotation()]
    
    
    var body: some View {
        //Get the size of the frame for scale
        GeometryReader{ geo in
            Map(
                coordinateRegion: $region, annotationItems: annotations) { _ in
                    MapAnnotation(coordinate: ScaledAnnotationView.usersLocation) {
                            //Size per kilometer or any unit, just change the converted unit.
                            let kilometerSize = (geo.size.height/region.spanLatitude.converted(to: .kilometers).value)
                        Circle()
                            .fill(Color.red.opacity(0.5))
                        //Keep it a circle
                            .frame(width: kilometerSize, height: kilometerSize)
                    }
                }
        }
    }
}

enter image description here

Here is Google maps distance measurement for confirmation of measurement

enter image description here

Upvotes: 4

Related Questions