2

In Apple’s iOS Reminders app, you can set a location area at which you would be reminded of your reminder. When you set the location area, you are allowed to draw a circle that covers the area of the location you want to be reminded at. How would I duplicate this feature. I particularly would like to know how to allow the user to resize the circle on a map view by dragging his finger on the edge of the circle. I have already figured out how to create the circle.

Here is my code so far:

import UIKit
import MapKit

class ViewController: UIViewController {

    // MARK: - Outlets

    @IBOutlet weak var mapView: MKMapView!

    // MARK: - Variables and Constants

    let regionRadius: CLLocationDistance = 1000
    let circularRegionRadius: CLLocationDistance = 500
    let locationManager = CLLocationManager()

    // MARK: - View

    override func viewDidLoad() {
        super.viewDidLoad()

        mapView.delegate = self
        locationManager.delegate = self

        locationManager.requestAlwaysAuthorization()
        locationManager.requestWhenInUseAuthorization()
        locationManager.desiredAccuracy = kCLLocationAccuracyBest

        locationManager.startUpdatingLocation()

        centerMapOnLocation(location: locationManager.location!)

    }

    // MARK: - Actions

    @IBAction func addRegion(_ sender: UILongPressGestureRecognizer) {

        print("addRegion(_:)")

        let longPress = sender

        let touchLocation = longPress.location(in: mapView)
        let coordinates = mapView.convert(touchLocation, toCoordinateFrom: mapView)
        let region = CLCircularRegion(center: coordinates, radius: circularRegionRadius, identifier: "geofence")
        mapView.removeOverlays(mapView.overlays)
        locationManager.startMonitoring(for: region)
        let circle = MKCircle(center: coordinates, radius: region.radius)
        mapView.add(circle)

    }

    // MARK: - Helper Functions

    func centerMapOnLocation(location: CLLocation) {

        let coordinateRegion = MKCoordinateRegionMakeWithDistance(location.coordinate,
                                                                  regionRadius, regionRadius)
        mapView.setRegion(coordinateRegion, animated: true)

    }

    // MARK: - Memory Warning

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

}

extension ViewController: MKMapViewDelegate {

    func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {

        print("mapView(_:rendererFor:_:)")

        guard let circelOverLay = overlay as? MKCircle else {return MKOverlayRenderer()}

        let circleRenderer = MKCircleRenderer(circle: circelOverLay)
        circleRenderer.strokeColor = .blue
        circleRenderer.fillColor = .blue
        circleRenderer.alpha = 0.2

        return circleRenderer

    }

}

extension ViewController: CLLocationManagerDelegate {

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {

        print("locationManager(_:didUpdateLocations:)")

        locationManager.stopUpdatingLocation()

        mapView.showsUserLocation = true

    }

}
daniel
  • 1,446
  • 3
  • 29
  • 65

1 Answers1

0

I have this exact same question but not a great solution. Below is what I came up with - it uses a long press gesture, calculates the distance between the circle and the long press point, creates a new MKCircle based off the new distance, removes the old one and adds the new one (and tap to create a circle at a given location). At first resizing the circle was very laggy. I looked at many things, including these to try and fix it: this, this, this, and this but didn't have much luck. There is also this and other answers that mention creating a custom class. I tried to create a custom MKCircle class that would allow me to modify the radius, but wasn't able to figure that out.
There is also this which I tried out but it was still laggy. Finally I used an integer counter to run the long press gesture only every every x times the function was called. This works much better, but it is still a little jumpy, and it feels hacky. The iOS reminders app circle adjustment is perfectly smooth, so it seems like there has to be a better way.

What is a better way to adjust the circle size?

On the reminders app, it seems like the circle is redrawn when the long press begins, and redrawn again when the long press ends. Are they replacing the MKCircle with a different class of shape (one that can be more easily manipulated) when the gesture begins, and then replacing it with an MKCircle overlay when the gesture ends? Is my whole approach misguided?

import SwiftUI
import CoreLocation
import MapKit

struct MapTestQ2: UIViewRepresentable {
    @Binding var centerCoordinate: CLLocationCoordinate2D
    
    @State private var mapView = MKMapView()
    @State private var thecircle = MKCircle()
    
   
    func makeUIView(context: Context) -> MKMapView {
        mapView.delegate = context.coordinator
        mapView.showsUserLocation = true
        let span = MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
        let region = MKCoordinateRegion(center: centerCoordinate, span: span)
        mapView.setRegion(region, animated: true)
        return mapView
    }
    
    func updateUIView(_ uiView: MKMapView, context: Context) {
        mapView.addOverlay(thecircle)
    }
    
    func makeCoordinator() -> Coordinator {
        return Coordinator(self)
    }
    
    class Coordinator: NSObject, MKMapViewDelegate, UIGestureRecognizerDelegate {
        @State var parent: MapTestQ2
        var tRecognizer = UITapGestureRecognizer()
        var lRecognizer = UILongPressGestureRecognizer()
        private var count = 0
        
        init(_ parent: MapTestQ2) {
            self.parent = parent
            super.init()
            self.tRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapHandler))
            self.tRecognizer.delegate = self
            self.parent.mapView.addGestureRecognizer(tRecognizer)
            self.lRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(pressHandler))
            self.lRecognizer.delegate = self
            self.parent.mapView.addGestureRecognizer(lRecognizer)
        }
        
        func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
            guard let circleoverlay = overlay as? MKCircle else {return MKOverlayRenderer()}
            let circleRenderer = MKCircleRenderer(circle: circleoverlay)
            circleRenderer.strokeColor = .blue
            circleRenderer.fillColor = .systemCyan
            circleRenderer.alpha = 0.5
            return circleRenderer
          
        }
        
        @objc func tapHandler(_ gesture: UITapGestureRecognizer) {
            print("Tapped")
            let location = tRecognizer.location(in: self.parent.mapView)
            let coordinate = self.parent.mapView.convert(location, toCoordinateFrom: self.parent.mapView)
            //let acircle = MKCircle(center: coordinate, radius: 500.0)
            self.parent.mapView.removeOverlay(self.parent.thecircle)
            self.parent.thecircle = MKCircle(center: coordinate, radius: 500.0)
        }
        @objc func pressHandler(_ gesture: UILongPressGestureRecognizer) {
            if count == 5 {
                let location = lRecognizer.location(in: self.parent.mapView)
                let coordinate = self.parent.mapView.convert(location, toCoordinateFrom: self.parent.mapView)
                let centerlat = self.parent.thecircle.coordinate.latitude
                let centerlon = self.parent.thecircle.coordinate.longitude
                let coord1 = CLLocation(latitude: centerlat, longitude: centerlon)
                let coord2 = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
                let distance = coord1.distance(from: coord2)
                print("Long Press \(distance)")
                self.parent.mapView.removeOverlay(self.parent.thecircle)
                let circ = MKCircle(center: CLLocationCoordinate2D(latitude: centerlat, longitude: centerlon), radius: distance)
                self.parent.thecircle = circ
                count = 0
            } else {
                count = count + 1
            }
            
        }
    }
}

struct SEQ2: View {
    @State private var testcoord = CLLocationCoordinate2D(latitude: 37.33461, longitude: -122.00898)
    var body: some View {
                   MapTestQ2(centerCoordinate: $testcoord)
    }
}