Search code examples
iosswiftmapkitmklocalsearch

Using MKLocalSearch CompletionHandler correctly


I am using mapkit to make an app that helps users find nearby restaurants (among other things) that appeal to them and am trying to utilize a MKLocalSearch. I have declared myMapItems, which is an array of MKMapItem, and am trying to set it equal to the results of my MKLocalSearch. When printing the results of my MKLocalSearch, I am provided with all ten MKMapItems, but when setting the myMapItems array equal to the results, the console is telling me that myMapItems is nil. So: var myMapItems: [MKMapItem]?

and then later, after defining "region" as an MKCoordinateRegion

let request = MKLocalSearchRequest()
    request.naturalLanguageQuery = "Restaurants"
    request.region = region!
    let search = MKLocalSearch(request: request)
    search.start { (response, error) in
        print(response?.mapItems)
        self.myMapItems = response?.mapItems
    }

So when I press the button that runs this code, the console prints response.mapItems, but fails to set my mapItems declared earlier equal to the search's results.

More detailed code:

import UIKit
import MapKit

class MapViewController: UIViewController, CLLocationManagerDelegate {
@IBOutlet weak var mapView: MKMapView!
let locationManager = CLLocationManager()

@IBOutlet weak var slider: UISlider!

@IBAction func searchButtonPressed(_ sender: Any) {
    search()
    print(self.mapItems)
}

var mapItems: [MKMapItem] = []

func search() {

    var region: MKCoordinateRegion?
    let userLocation = CLLocation(latitude: (locationManager.location?.coordinate.latitude)!, longitude: (locationManager.location?.coordinate.longitude)!)

    //Function for translating a CLLocationCoordinate2D by x meters
    func locationWithBearing(bearing:Double, distanceMeters:Double, origin:CLLocationCoordinate2D) -> CLLocationCoordinate2D {
        let distRadians = distanceMeters / (6372797.6)

        let rbearing = bearing * Double.pi / 180.0

        let lat1 = origin.latitude * Double.pi / 180
        let lon1 = origin.longitude * Double.pi / 180

        let lat2 = asin(sin(lat1) * cos(distRadians) + cos(lat1) * sin(distRadians) * cos(rbearing))
        let lon2 = lon1 + atan2(sin(rbearing) * sin(distRadians) * cos(lat1), cos(distRadians) - sin(lat1) * sin(lat2))

        return CLLocationCoordinate2D(latitude: lat2 * 180 / Double.pi, longitude: lon2 * 180 / Double.pi)
    }

    //Function for generating the search region within user's specified radius
    func generateRandomSearchRegionWithinSpecifiedRadius() {
        //Get user location
        let userCurrentLocation = CLLocationCoordinate2DMake(userLocation.coordinate.latitude, userLocation.coordinate.longitude)

        //Set randomLocationWithinSearchRadius to the user's current location translated in a randomly selected direction by a distance within x miles as specified by the user's slider
        let randomLocationWithinSearchRadius = locationWithBearing(bearing: Double(arc4random_uniform(360)), distanceMeters: Double(arc4random_uniform(UInt32(slider.value * 1609.34))), origin: userCurrentLocation)

        //Set region to an MKCoordinateRegion with this new CLLocationCoordinate2D as the center, searching within 3 miles
        region = MKCoordinateRegionMakeWithDistance(randomLocationWithinSearchRadius, 4828.03, 4828.03)
        print("\nRegion:\n     Lat: \(region?.center.latitude ?? 0)\n     Long: \(region?.center.longitude ?? 0)\n     Radius: \(round((region?.span.latitudeDelta)! * 69))")
    }

    //Generate the random searching region within specified radius
    generateRandomSearchRegionWithinSpecifiedRadius()

    //Find distance between userLocation and our generated search location
    let distanceBetweenLocations = CLLocation(latitude: (region?.center.latitude)!, longitude: (region?.center.longitude)!).distance(from: CLLocation(latitude: (userLocation.coordinate.latitude), longitude: (userLocation.coordinate.longitude)))

    //Compare distance between locations with that requested by user's slider
    print("\n     Distance between locations: \(distanceBetweenLocations / 1609.34)\n     Slider value: \(slider.value)\n\n\n")

    //If the function generates a location whose distance from the user is further than that requested by the slider, the function will repeat
    while distanceBetweenLocations > Double((slider.value) * 1609.34) {
        generateRandomSearchRegionWithinSpecifiedRadius()
    }

    let request = MKLocalSearchRequest()
    request.naturalLanguageQuery = "Restaurants"
    request.region = region!
    let search = MKLocalSearch(request: request)
    search.start { (response, error) in
        //print(response?.mapItems)
        for item in (response?.mapItems)! {
            self.mapItems.append(item)
        }
    }
}

The entire class should anybody need it:

import UIKit
import MapKit

class MapViewController: UIViewController, CLLocationManagerDelegate {
@IBOutlet weak var mapView: MKMapView!
let locationManager = CLLocationManager()

@IBOutlet weak var pickerTextField: UITextField!
let actions = ["A place to eat", "Something fun to do", "A park", "A club or bar"]

@IBOutlet weak var slider: UISlider!

@IBOutlet weak var distanceLabel: UILabel!

@IBOutlet weak var searchButton: UIButton!

override func viewDidLoad() {
    super.viewDidLoad()

    pickerTextField.loadDropdownData(data: actions)
    // Do any additional setup after loading the view, typically from a nib.

    // Core Location Manager asks for GPS location
    locationManager.delegate = self
    locationManager.desiredAccuracy = kCLLocationAccuracyBest
    locationManager.requestWhenInUseAuthorization()
    locationManager.startMonitoringSignificantLocationChanges()

    // Check if the user allowed authorization
    if (CLLocationManager.authorizationStatus() == CLAuthorizationStatus.authorizedWhenInUse ||
        CLLocationManager.authorizationStatus() == CLAuthorizationStatus.authorizedAlways)
    {
        // set initial location as user's location
        let initialLocation = CLLocation(latitude: (locationManager.location?.coordinate.latitude)!, longitude: (locationManager.location?.coordinate.longitude)!)
        centerMapOnLocation(location: initialLocation, regionRadius: 1000)
        print("Latitude: \(locationManager.location?.coordinate.latitude), Longitude: \(locationManager.location?.coordinate.longitude)")
    } else {
        print("Failed")
    }

    let span : MKCoordinateSpan = MKCoordinateSpanMake(0.1, 0.1)
    let location : CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: 38.645933, longitude: -90.503613)

    let pin = PinAnnotation(title: "Gander", subtitle: "You can get food here", coordinate: location)
    mapView.addAnnotation(pin)
}

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


func centerMapOnLocation(location: CLLocation, regionRadius: Double) {
    let regionRadius = CLLocationDistance(regionRadius)

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

@IBAction func sliderAdjusted(_ sender: Any) {
    var int = Int(slider.value)
    distanceLabel.text = "\(int) miles"
}

@IBAction func searchButtonPressed(_ sender: Any) {
    search()
    print(self.mapItems)
    //chooseRandomSearchResult(results: self.mapItems!)

}

var mapItems: [MKMapItem] = []

func search() {

    var region: MKCoordinateRegion?
    let userLocation = CLLocation(latitude: (locationManager.location?.coordinate.latitude)!, longitude: (locationManager.location?.coordinate.longitude)!)

    //Function for translating a CLLocationCoordinate2D by x meters
    func locationWithBearing(bearing:Double, distanceMeters:Double, origin:CLLocationCoordinate2D) -> CLLocationCoordinate2D {
        let distRadians = distanceMeters / (6372797.6)

        let rbearing = bearing * Double.pi / 180.0

        let lat1 = origin.latitude * Double.pi / 180
        let lon1 = origin.longitude * Double.pi / 180

        let lat2 = asin(sin(lat1) * cos(distRadians) + cos(lat1) * sin(distRadians) * cos(rbearing))
        let lon2 = lon1 + atan2(sin(rbearing) * sin(distRadians) * cos(lat1), cos(distRadians) - sin(lat1) * sin(lat2))

        return CLLocationCoordinate2D(latitude: lat2 * 180 / Double.pi, longitude: lon2 * 180 / Double.pi)
    }

    //Function for generating the search region within user's specified radius
    func generateRandomSearchRegionWithinSpecifiedRadius() {
        //Get user location
        let userCurrentLocation = CLLocationCoordinate2DMake(userLocation.coordinate.latitude, userLocation.coordinate.longitude)

        //Set randomLocationWithinSearchRadius to the user's current location translated in a randomly selected direction by a distance within x miles as specified by the user's slider
        let randomLocationWithinSearchRadius = locationWithBearing(bearing: Double(arc4random_uniform(360)), distanceMeters: Double(arc4random_uniform(UInt32(slider.value * 1609.34))), origin: userCurrentLocation)

        //Set region to an MKCoordinateRegion with this new CLLocationCoordinate2D as the center, searching within 3 miles
        region = MKCoordinateRegionMakeWithDistance(randomLocationWithinSearchRadius, 4828.03, 4828.03)
        print("\nRegion:\n     Lat: \(region?.center.latitude ?? 0)\n     Long: \(region?.center.longitude ?? 0)\n     Radius: \(round((region?.span.latitudeDelta)! * 69))")
    }

    //Generate the random searching region within specified radius
    generateRandomSearchRegionWithinSpecifiedRadius()

    //Find distance between userLocation and our generated search location
    let distanceBetweenLocations = CLLocation(latitude: (region?.center.latitude)!, longitude: (region?.center.longitude)!).distance(from: CLLocation(latitude: (userLocation.coordinate.latitude), longitude: (userLocation.coordinate.longitude)))

    //Compare distance between locations with that requested by user's slider
    print("\n     Distance between locations: \(distanceBetweenLocations / 1609.34)\n     Slider value: \(slider.value)\n\n\n")

    //If the function generates a location whose distance from the user is further than that requested by the slider, the function will repeat
    while distanceBetweenLocations > Double((slider.value) * 1609.34) {
        generateRandomSearchRegionWithinSpecifiedRadius()
    }

    let request = MKLocalSearchRequest()
    request.naturalLanguageQuery = "Restaurants"
    request.region = region!
    let search = MKLocalSearch(request: request)
    search.start { (response, error) in
        //print(response?.mapItems)
        for item in (response?.mapItems)! {
            self.mapItems.append(item)
        }
    }

func chooseRandomSearchResult(results: [MKMapItem]) -> MKMapItem {
    let numberOfItems = results.count
    let randomNumber = Int(arc4random_uniform(UInt32(numberOfItems)))
    return results[randomNumber]
}

}


Solution

  • The problem would be that search.start is ASYNCHRONOUS, it will start the request and return before results are done. Assume you need to work in completion handler. Replace:

    @IBAction func searchButtonPressed(_ sender: Any) {
        search()
        print(self.mapItems)
        //chooseRandomSearchResult(results: self.mapItems!)
    
    }
    

    To: (Remove the usage of the results as they are not there yet, literally the search has been fired but has not returned)

     @IBAction func searchButtonPressed(_ sender: Any) {
            search()
        }
    

    AND do the actual work in the callback:

    //Still inside search()
    search.start { (response, error) in
        //Executed after search() has already returned
        print(response?.mapItems)
        self.mapItems = response?.mapItems
        chooseRandomSearchResult(results: self.mapItems!)
    }
    //Still inside search()
    

    As you can see the code marked: //Executed after search() has already returned ALWAYS executes after //Still inside search() even if it is before it in the function

    (As of iOS 11.x, the documentation guarantees the completion handler for MKLocalSearch.start will be on the main thread)