David Chopin
David Chopin

Reputation: 3064

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]
}

}

Upvotes: 2

Views: 1939

Answers (1)

Mitchell Currie
Mitchell Currie

Reputation: 2809

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)

Upvotes: 1

Related Questions