0

Environment

  • Xcode 8
  • Swift 3
Problem Statement
    I want to be able to determine if a user taps on a MKPointAnnotation and then extract information (like title and subtitle) from that annotation for use within my app.
    I imagine this is not terribly difficult, but I'm a bit lost in terms of what I need to do / what various classes / objects / methods / etc. I need to use to do this.
    So I'm looking for pointers / guidance - code is welcome, but at this point the pointers / guidance would be a significant step forward for me.
Code Snippets
    Abridged version of the code thus far (trying to limit it to just the relevant pieces)
class NewLocationViewController: UIViewController, CLLocationManagerDelegate, UITextFieldDelegate {

    //... various @IBOutlet's for text fields, buttons, etc. ...
    @IBOutlet weak var map:         MKMapView!

    var coords:            CLLocationCoordinate2D?
    var locationManager:   CLLocationManager = CLLocationManager()
    var myLocation:        CLLocation!
    var annotation:        MKPointAnnotation!
    var annotationList:    [MKPointAnnotation] = []
    var matchingItems:     [MKMapItem] = [MKMapItem]()

    override func viewDidLoad() {
        super.viewDidLoad()

        //... text field delegates, and other initilizations ...
        locationManager.requestWhenInUseAuthorization()
        if CLLocationManager.locationServicesEnabled() {
            locationManager.desiredAccuracy = kCLLocationAccuracyBest
            locationManager.delegate        = self
        }
        myLocation       = nil
        //... other initializations...
    }

    // Search for things that match what my app is looking for ("<search string>")
    func performSearch() {
        annotationList.removeAll() // clear list
        matchingItems.removeAll()  // clear list 
        var closest                  = MKMapItem()
        var distance                 = 10000.0
        let request                  = MKLocalSearchRequest()
        let span                     = MKCoordinateSpan(latitudeDelta: 0.001, longitudeDelta: 0.001)
        request.naturalLanguageQuery = "<search string>"
        request.region               = MKCoordinateRegionMake(myLocation.coordinate, span)
        let search                   = MKLocalSearch(request: request)

        if search.isSearching {
            search.cancel()
        }
        search.start(completionHandler: {
            (_ response, _ error) in
            if error != nil {
                self.showAlert(msg: "Error occurred in search\nERROR: \(error?.localizedDescription)")
            }
            else if response!.mapItems.count == 0 {
                self.showAlert(msg: "No matches found") 
            }
            else {
                for item in response!.mapItems {

                    // Track the closest placemark to our current [specified] location
                    let (distanceBetween, prettyDistance) = self.getDistance(loc1: self.myLocation, loc2: item.placemark.location!)
                    let addrObj                           = self.getAddress(placemark: item.placemark)
                    //... some code omitted ...

                    // Add markers for all the matches found
                    self.matchingItems.append(item as MKMapItem)
                    let annotation        = MKPointAnnotation()
                    annotation.coordinate = item.placemark.coordinate
                    annotation.title      = item.name
                    annotation.subtitle   = "\(addrObj.address!) (\(prettyDistance))"
                    self.map.addAnnotation(annotation)
                    self.annotationList.append(annotation) 
                }

                //... some code omitted ...
            }
        })
    }

    //... code for getDistance(), getAddress() omitted for brevity - they work as designed ...
    //... other code omitted as not being relevant to the topic at hand
}

I imagine that I will need to override touchesEnded and possibly touchesBegan and maybe touchesMoved in order to detect the tap.
What I cannot figure out is how to compare a touch's location (represented as X/Y coordinates on the screen) to an MKPointAnnotation's or MKMapItem's location (which is represented as latitude/longitude coordinates on a map)
So - that's kind of where I'm currently stuck. I searched various terms on the web but wasn't able to find anything that [simplisticly] answerwed my question - and in Swift code format (there were a number of postings that looked like they might help, but the code presented wasn't in Swift and I don't do the translation that easily).

UPDATE (19:48 ET)

class NewLocationViewController: UIViewController, CLLocationManagerDelegate, UITextFieldDelegate, UIGestureRecognizerDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
        //...other code...
        let tapHandler = UITapGestureRecognizer(target: self, action: Selector(("handleTap:"))) //<<<== See notes below
        tapHandler.numberOfTapsRequired    = 1
        tapHandler.numberOfTouchesRequired = 1
        tapHandler.delegate                = self
        print("A")//#=#
        map.addGestureRecognizer(tapHandler)
        map.isUserInteractionEnabled       = true
        print("B")//#=#
    }

    func handleTap(tap: UITapGestureRecognizer) {
        print("ARRIVED")//#=#
        let here = tap.location(in: map)
        print("I AM HERE: \(here)")//#=#
    }

    //...
}

With regard to the declaration / definition of tapHandler, I tried the following:

    let tapHandler = UITapGestureRecognizer(target: self, action: "handleTap:")
    let tapHandler = UITapGestureRecognizer(target: self, action: Selector("handleTap:")) 
    let tapHandler = UITapGestureRecognizer(target: self, action: Selector(("handleTap:"))) // supresses warning

The first two caused a warning to show up in Xcode, the last simply supresses the warning:

[W] No method declared with Objective-C selector 'handleTap:'

When I run my app and tap on a pin - I get the following in my log:

A
B
libc++abi.dylib: terminating with uncaught exception of type NSException

Which would seem (to me) to indicate that the general setup in viewDidLoad is okay, but as soon as it tries to handle the tap, it dies without ever getting to my handleTap function - and thus the warning (shown above) would seem to be far more serious.

So, I'm not sure if I can count this as making progress, but I'm trying...

Community
  • 1
  • 1
Adam Stoller
  • 933
  • 8
  • 16

1 Answers1

0

Thanks to this MKAnnotationView and tap detection I was able to find a solution. My code changes from those originally posted:

class NewLocationViewController: UIViewController, CLLocationManagerDelegate, UITextFieldDelegate, UIGestureRecognizerDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
        //...other code...
        let tapHandler = UITapGestureRecognizer() //<<<== No parameters
        tapHandler.numberOfTapsRequired    = 1
        tapHandler.numberOfTouchesRequired = 1
        tapHandler.delegate                = self
        map.addGestureRecognizer(tapHandler)
        map.isUserInteractionEnabled       = true
    }

    // Not sure who calls this and requires the Bool response, but it seems to work...
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
        return self.handleTap(touch: touch).count > 0
    }

    // Major Changes
    private func handleTap(touch: UITouch) -> [MKAnnotationView] {
        var tappedAnnotations: [MKAnnotationView] = []
        for annotation in self.map.annotations {
            if let annotationView: MKAnnotationView = self.map.view(for: annotation) {
                let annotationPoint = touch.location(in: annotationView)
                if annotationView.bounds.contains(annotationPoint) {
                    self.name.text    = annotationView.annotation?.title!
                    let addr          = AddrInfo(composite: ((annotationView.annotation?.subtitle)!)!)
                    self.address.text = addr.street!
                    self.city.text    = addr.city!
                    self.state.text   = addr.state!
                    self.zipcode.text = addr.zip!
                    tappedAnnotations.append(annotationView)
                    break
                }
            }
        }
        return tappedAnnotations
    }

    //...
}

The AddrInfo piece is my own little subclass that, among other things, takes a string like "1000 Main St., Pittsburgh, PA 15212, United States" and breaks it into the individual pieces so that they can be accessed, well, individually (as indicated in the code above).

There might be an easier, or better, way to achieve what I was looking for - but the above does achieve it, and so I consider it to be the answer for my issue.

Community
  • 1
  • 1
Adam Stoller
  • 933
  • 8
  • 16