Search code examples
iosswiftswiftuimapkit

How can I indicate a radius around UserLocation in MapKit?


I am using MapKit + iOS 17 features to display a map with the user's location. I would like to draw a 30-metre radius around the UserLocation. How can I do this? I've seen some solutions from 12 years ago but I assume there is a better way to do this now.

struct Home: View {
    
    @State private var position: MapCameraPosition = .userLocation(fallback: .automatic)
    @State private var visibleRegion: MKCoordinateRegion?
    @State private var searchResults: [MKMapItem] = []
    @State private var selectedResult: MKMapItem?
    @State private var showDetails: Bool = false
    @State private var lookAroundScene: MKLookAroundScene?
    
    let locationManager = CLLocationManager()
    
    var body: some View {
        
        Map(position: $position, selection: $selectedResult) {
            
            let customLocations: [MKMapItem] = [
                createMapItem(name: "Via Palamos", coordinate: .viaPalamos),
                createMapItem(name: "Larry Way", coordinate: .larryWay),
            ]
            
            
            ForEach(customLocations, id: \.self) { result in
                Marker(result.name ?? "Unknown Location", systemImage: "binoculars.fill", coordinate: result.placemark.coordinate)
                    .tint(.blue)
            }
            .annotationTitles(.hidden)
            
            
            UserAnnotation()
            
        }
        .mapStyle(.standard(pointsOfInterest: .excludingAll))
        .mapStyle(.standard(elevation: .realistic))
        .sheet(isPresented: $showDetails, onDismiss: {
            withAnimation(.snappy) {
                
            }
        }, content: {
            MapDetails()
                .presentationDetents([.height(750)])
                .presentationBackgroundInteraction(.enabled(upThrough: .height(750)))
                .presentationCornerRadius(25)
                .interactiveDismissDisabled(true)
        })
        .onAppear {
            locationManager.requestWhenInUseAuthorization()
        }
        //MARK: This could help animate it nicely, i think safe inset view is causing interference atm though
        //        .animation(.easeIn)
        .onChange(of: searchResults) {
            position = .automatic
        }
        .onChange(of: selectedResult) { oldValue, newValue in
            /// Displaying Details about the Selected Place
            showDetails = newValue != nil
            /// Fetching Look Around Preview, when ever selection Changes
            fetchLookAroundPreview()
        }
        .onMapCameraChange { context in
            visibleRegion = context.region
        }
        .mapControls {
            VStack {
                MapUserLocationButton()
                //MARK: Can I make it pitch more by defualt? and make it the automaitic mode on initilisation
                MapPitchToggle()
                MapCompass()
                MapScaleView()
            }
            .buttonBorderShape(.circle)
        }
    }
}

How can I calculate the 30-metre radius around the user's location and display that on the map? It would need to update itself as the user's location changes too. So when there user is moving around, the 30-metre radius circle moves with them.


Solution

  • A simple solution would be to have a MapCircle that changes its location according to the location updates from a CLLocationManager.

    Here is a simple example that shows a semi-transparent red circle around the user's location.

    @Observable
    class LocationTracker: NSObject, CLLocationManagerDelegate {
        var location: CLLocationCoordinate2D? = nil
        let manager = CLLocationManager()
        override init() {
            super.init()
            manager.delegate = self
            manager.requestWhenInUseAuthorization()
        }
        
        func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
            location = locations.first?.coordinate
        }
        
        deinit {
            manager.stopUpdatingLocation()
        }
        
        func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
            print("Changed", manager.authorizationStatus.rawValue)
            if manager.authorizationStatus == .authorizedWhenInUse {
                manager.startUpdatingLocation()
            }
        }
        
    }
    
    struct ContentView: View {
        @State var manager: LocationTracker?
        var body: some View {
            Map {
                UserAnnotation.init { loc in
                    Circle()
                }
                if let c = manager?.location {
                    MapCircle(center: c, radius: 30)
                        .foregroundStyle(.red.opacity(0.5))
                }
            }
            .onAppear {
                manager = LocationTracker()
            }
        }
    }
    

    That said, the circle only updates once or twice a second, because there is a limit to how often didUpdateLocations is called.