How to set properties within a Closure/Block in Swift - ios

I currently have two fields/properties within my view controller. We are using the calculateDirectionsWithCompletionHandler and trying to set my fields to the value of route.distance and route.expectedTravelTime. Here is the code for that:
func calculateDistanceAndEta(locationCoordinate: CLLocationCoordinate2D) {
let currentLocMapItem = MKMapItem.mapItemForCurrentLocation();
let selectedPlacemark = MKPlacemark(coordinate: locationCoordinate, addressDictionary: nil);
let selectedMapItem = MKMapItem(placemark: selectedPlacemark);
let mapItems = [currentLocMapItem, selectedMapItem];
let request: MKDirectionsRequest = MKDirectionsRequest()
request.transportType = MKDirectionsTransportType.Walking;
request.setSource(currentLocMapItem)
request.setDestination(selectedMapItem);
var directions: MKDirections = MKDirections(request: request);
var distsanceLabelTest = ""
var etaLabelTest = ""
directions.calculateDirectionsWithCompletionHandler { (response, error) -> Void in
if (error == nil) {
if (response.routes.count > 0) {
var route: MKRoute = response.routes[0] as! MKRoute;
// route.distance = distance
// route.expectedTravelTime = eta
println("\(route.distance)")
distsanceLabelTest = "\(route.distance)"
etaLabelTest = "\(route.expectedTravelTime)"
}
} else {
println(error)
}
}
println(distsanceLabelTest)
println(etaLabelTest)
self.distanceLabelString = distsanceLabelTest
self.etaLabelString = etaLabelTest
}
However, we can't seem to set any of the variables as it just returns nil. How do we set our class fields to the values of route.distance and route.expectedTravelTime.

we can't seem to set any of the variables as it just returns nil.
The point of providing a completion block is that calculateDirectionsWithCompletionHandler runs asynchronously and executes the completion routine when it's ready. So your distance and expectedTravelTime properties will indeed be unchanged immediately after calculateDistanceAndEta returns because the process started by calculateDirectionsWithCompletionHandler may not have finished by then. Your completion block will be run when it does finish. If you need to take some action when the properties are set, put that code in your completion block.

Related

Trying to use reverseGeocodeLocation, but completionHandler code is not being executing

The issue is that the code inside the completionHandler block is never run; I used breakpoints and the program would skip over the completion handler block in build mode
Below are two functions used within PinALandPlaceMark, where most of my code is located
func generateRandomWorldLatitude()-> Double{
let latitude = Double.random(in: -33 ..< 60)
return latitude
}
func generateRandomWorldLongitude()-> Double{
let longitude = Double.random(in: -180 ..< 180)
return longitude
}
func PinALandPlaceMark() -> MKAnnotation {
var LandBasedCountryHasYetToBeFound : (Bool, CLLocationDegrees?, CLLocationDegrees?)
LandBasedCountryHasYetToBeFound = (false,nil,nil)
let randomPinLocation = MKPointAnnotation()
repeat{
if LandBasedCountryHasYetToBeFound == (false,nil,nil){
let latitude: CLLocationDegrees = generateRandomWorldLatitude()
let longitude: CLLocationDegrees = generateRandomWorldLongitude()
let randomCoordinate = CLLocation(latitude: latitude, longitude: longitude)
let geocoder = CLGeocoder()
geocoder.reverseGeocodeLocation(randomCoordinate, completionHandler: { (placemarks, error) -> Void in
if error != nil{print(error)}else{
guard let placemark = placemarks?.first else{return}
//check which placemark property exists, store in the 0 alpha labelForClosure
if let countryExists = placemark.country {
LandBasedCountryHasYetToBeFound = (true,latitude,longitude)
//country = countryExists as String
// viewController.labelForClosure.text = countryExists
print(" Country Exists!: \(countryExists)")
print(" randomCoordinate \(randomCoordinate)")
}
}
})
}
// print("The country found was on land. This statement is \(LandBasedCountryHasYetToBeFound.occursInCountry)")
else{
let coordinatesOfrandomPinLocation = CLLocationCoordinate2D(latitude: LandBasedCountryHasYetToBeFound.1!, longitude: LandBasedCountryHasYetToBeFound.2!)
randomPinLocation.title = ""//LandBasedCountryHasYetToBeFound.countryName
randomPinLocation.coordinate = coordinatesOfrandomPinLocation
// viewController.mapView.addAnnotation(randomPinLocation)
}
}while LandBasedCountryHasYetToBeFound.0 == false
print("randomPin has been returned, now use pin function inside placemark declaration")
return randomPinLocation
}
Your main problem is that your CLGeocoder instance is held in a local variable inside the loop; This means that it will be released before it has completed its task.
You have a couple of other issues too which would cause you problems even if the reverse geo-coding did complete.
The main one is that you are checking for loop termination using a boolean that is set inside the closure; The closure will execute asynchronously, so the loop will have executed many more times before the boolean is set to true in the case where an address is found.
The second problem is related to and made worse by this; reverse geocoding is rate limited. If you submit too many requests too quickly, Apple's servers will simply return an error. Even if you did wait for the first response before submitting a second, your chances of hitting land at random are pretty low, so you will probably hit this limit pretty quickly.
Ignoring the rate limit problem for the moment, you can use a recursive function that accepts a completion handler rather than using a loop and trying to return a value.
var geoCoder = CLGeocoder()
func pinALandPlaceMark(completion: #escaping (Result<MKAnnotation, Error>) -> Void) {
let latitude: CLLocationDegrees = generateRandomWorldLatitude()
let longitude: CLLocationDegrees = generateRandomWorldLongitude()
let randomCoordinate = CLLocation(latitude: latitude, longitude: longitude)
geocoder.reverseGeocodeLocation(randomCoordinate) { (placemarks, error) in
guard error == nil else {
completion(nil,error)
return error
}
if let placemark = placemarks.first, let _ = placemark.country {
let randomPinLocation = MKPointAnnotation()
randomPinLocation.coordinate = randomCoordinate.coordinate
completionHandler(randomPinLocation,nil)
} else {
pinALandPlaceMark(completion:completion)
}
}
}
The first thing we do is declare a property to hold the CLGeocoder instance so that it isn't released.
Next, this code checks to see if a placemark with a country was returned. If not then the function calls itself, passing the same completion handler, to try again. If an error occurs then the completion handler is called, passing the error
To use it, you would say something like this:
pinALandPlaceMark() { result in
switch result {
case .success(let placemark):
print("Found \(placemark)")
case .failure(let error):
print("An error occurred: \(error)")
}
}

After deriving the driving distance between two points using MKDirections (Swift 4) how to access the distance value outside the closure? [duplicate]

This question already has answers here:
How do i return coordinates after forward geocoding?
(3 answers)
Wait for completion block of writeImageToSavedPhotosAlbum by semaphore
(1 answer)
Closed 4 years ago.
I have written something like this to calculate the driving distance between 2 points / locations.
Method Implementation:
Class 1:
static func calculateDistance(_ location1 : CLLocationCoordinate2D, location2: CLLocationCoordinate2D, completion: #escaping (_ distance: CLLocationDistance?) -> ()) {
let start = MKMapItem(placemark: MKPlacemark(coordinate: location1))
let destination = MKMapItem(placemark: MKPlacemark(coordinate: location2))
let request = MKDirectionsRequest()
request.source = start
request.destination = destination
request.requestsAlternateRoutes = false
let direction = MKDirections(request: request)
var distanceInMiles: CLLocationDistance?
direction.calculate { (response, error) in
if let response = response, let route = response.routes.first {
distanceInMiles = route.distance * 0.000621371
completion(distanceInMiles)
}
}
}
Usage Question
Class 2:
How do I access the distance value in a different class? For example, I have a parameterized init, where the third parameter "dist" is of type CLLocationDistance. What I am trying to achieve is to access the distance value from the calculateDistance method of Class1
let assigningDistValue = Class1(coordinate: location, secondParam: paramValue, dist:finalDistance!)
I have pretty much read all suggested solutions related to this problem and nothing helped.
You cannot access finalDistance after the closure, because the code runs in this order:
var finalDistance: CLLocationDistance?
// 1:
let calculatedDistance = Class1.calculateDistance(location, location2: secondlocation) { (distance) in
// 3:
guard let distanceInMiles = distance else { return }
print("This is to print distance in miles", distanceInMiles)
finalDistance = calculatedDistance
}
// 2:
let assigningDistValue = Class1(coordinate: location, secondParam: paramValue, dist:finalDistance!)
Just move the let line into the end of the asynchronous material:
let calculatedDistance = Class1.calculateDistance(location, location2: secondlocation) { (distance) in
guard let distanceInMiles = distance else { return }
print("This is to print distance in miles", distanceInMiles)
finalDistance = calculatedDistance
// put it here
let assigningDistValue = Class1(coordinate: location, secondParam: paramValue, dist:finalDistance!) {
// and on we go...
}
}
Or else, use another completion block, just you did in the first code you showed. This is all correct in the first code, but then in the second code all that knowledge of what asynchronous means appears to be forgotten.

Getting " modifying the autolayout engine from a background thread after the engine was accessed from the main thread" even while using DispatchQueue

Hello I'm trying to create a simple app that gets annotations from my web server and then propagates the map with them. The only problem is when I call the function bob 30 seconds later to get a new annotation from another location it gives me the error above, I tried to fix it using DispatchQueue.main.async to no avail. Any help is appreciated.
Here is the function in question
// this is the test to see if it can add a new annotation after 30 seconds
if bob == 30{
let user_lat_temp = 26.7709
let user_lng_temp = -80.1067
DispatchQueue.main.async() {
// Do stuff to UI
self.GetAnnotations(lat: user_lat_temp, lng: user_lng_temp)
}
// reset it to see if it breaks
bob = 0
}
bob = bob + 1
print("bob: ", bob)
}
Here is the full code
import UIKit
import MapKit
import CoreLocation
class ViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
#IBOutlet weak var mapView: MKMapView!
let access_token = ""
let manager = CLLocationManager()
var firstTime = 1
var user_lat = 0.0
var user_lng = 0.0
var bob = 0
override func viewDidLoad() {
super.viewDidLoad()
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.requestWhenInUseAuthorization()
manager.startUpdatingLocation()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let userLocation = locations[0]
user_lat = userLocation.coordinate.latitude
user_lng = userLocation.coordinate.longitude
self.mapView.showsUserLocation = true
if firstTime == 1{
GetAnnotations(lat: user_lat, lng: user_lng)
firstTime = 0
}
// this is the test to see if it can add a new annotation after 30 seconds
if bob == 30{
let user_lat_temp = 26.7709
let user_lng_temp = -80.1067
DispatchQueue.main.async() {
// Do stuff to UI
self.GetAnnotations(lat: user_lat_temp, lng: user_lng_temp)
}
// reset it to see if it breaks
bob = 0
}
bob = bob + 1
print("bob: ", bob)
}
func GetAnnotations(lat: Double, lng: Double){
guard let url = URL(string: "http://192.168.1.10:7888/api/?controller=location&action=get_locations") else {return}
var request = URLRequest(url: url)
request.httpMethod = "POST"
let postString = "access_token=\(access_token)&lat=\(lat)&lng=\(lng)";
request.httpBody = postString.data(using: String.Encoding.utf8)
URLSession.shared.dataTask(with: request) { (data, response, err) in
if let error = err {
print("the server is not responding \(error)")
}
if let response = response {
// if the user has a bad access token or is logged out
if let httpResponse = response as? HTTPURLResponse {
if httpResponse.statusCode == 401{
print("bad access token")
return
}
}else{
print("the server is not responding")
}
print(response)
guard let data = data else { return }
// parse the json for the locations
do {
let mapJSON = try JSONDecoder().decode(parseJsonLocations.self, from: data)
let user_id = mapJSON.user_info.user_id
print(user_id)
print(mapJSON.locations.count)
// do map
let distanceSpan:CLLocationDegrees = 10000
let userLocation:CLLocationCoordinate2D = CLLocationCoordinate2DMake(lat, lng)
self.mapView.setRegion(MKCoordinateRegionMakeWithDistance(userLocation, distanceSpan, distanceSpan), animated: true)
self.mapView.delegate = self
var i = 0
while i < mapJSON.locations.count {
let location_id = mapJSON.locations[i].location_id
let location_name = mapJSON.locations[i].location_name
let location_lat = mapJSON.locations[i].lat
let location_lng = mapJSON.locations[i].lng
let locationsLocation:CLLocationCoordinate2D = CLLocationCoordinate2DMake(location_lat, location_lng)
let subtitle = "location_id: \(location_id)"
let userAnnotation = Annotation(title: location_name, subtitle: subtitle, coordinate: locationsLocation)
self.mapView.addAnnotation( userAnnotation )
i = i + 1
}
} catch {
print("error trying to convert data to JSON")
print(error)
}
}
}.resume()
}
}
There are a lot of other places where you are not paying attention to the question of what thread you might be on.
You are saying
if firstTime == 1 {
GetAnnotations( // ...
without making sure you're on the main thread.
And then, inside GetAnnotations, you are saying
self.mapView.setRegion
// ... and all that follows ...
without making sure you're on the main thread.
I'm not saying you're not on the main thread at those moments, but there is no reason to think that you are, either. You should check and get it sorted out.
You have the right idea; explicitly dispatch the UI updates on the main queue, unfortunately you have dispatched the wrong function.
GetAnnotations (Which, by convention should be getAnnotations) makes an asynchronous network call via the URLSession DataTask, with the results returned in the completion handler. With network operations such as these there is a good chance that the completion handler will not be called on the main queue and that is the case here.
Since the completion handler is not executing on the main queue and you update the UI, you get the error message.
You need to dispatch those UI operations on the main queue
For example:
guard let data = data else { return }
DispatchQueue.main.async {
// parse the json for the locations
do {
let mapJSON = try JSONDecoder().decode(parseJsonLocations.self, from: data)
let user_id = mapJSON.user_info.user_id
print(user_id)
print(mapJSON.locations.count)
// do map
let distanceSpan:CLLocationDegrees = 10000
let userLocation:CLLocationCoordinate2D = CLLocationCoordinate2DMake(lat, lng)
self.mapView.setRegion(MKCoordinateRegionMakeWithDistance(userLocation, distanceSpan, distanceSpan), animated: true)
self.mapView.delegate = self
var i = 0
while i < mapJSON.locations.count {
let location_id = mapJSON.locations[i].location_id
let location_name = mapJSON.locations[i].location_name
let location_lat = mapJSON.locations[i].lat
let location_lng = mapJSON.locations[i].lng
let locationsLocation:CLLocationCoordinate2D = CLLocationCoordinate2DMake(location_lat, location_lng)
let subtitle = "location_id: \(location_id)"
let userAnnotation = Annotation(title: location_name, subtitle: subtitle, coordinate: locationsLocation)
self.mapView.addAnnotation( userAnnotation )
i = i + 1
}
} catch {
print("error trying to convert data to JSON")
print(error)
}
}
In the case of the location manager delegate call, the documentation states:
The methods of your delegate object are called from the thread in which you started the corresponding location services. That thread must itself have an active run loop, like the one found in your application’s main thread.
So, as long as you called startUpdatingLocation from the main queue, your delegate callbacks will be on the main queue

Apple Mapkit calculateETAWithCompletionHandler loop through array

I am making an application where when you click a button it populates a tableview with all of the Users and their estimated ETA from you. I keep running into an issue when I'm looping through my users and getting their location it doesn't make the Array in the same order that its looping through and therefore the times become all mixed up. I've tried several different ways of looping through them but it seems like when it gets the directions.calculateETA section it is delayed and runs at different times. Can anyone help me run through this efficiently so the loop doesn't run out of order?
for user in usersArray {
var userLocations:CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: user["location"].latitude, longitude: user["location"].longitude)
ETARequest(userLocations)
}
func ETARequest(destination:CLLocationCoordinate2D) {
let request = MKDirectionsRequest()
if let currentLocation = self.manager.location?.coordinate {
request.source = MKMapItem(placemark: MKPlacemark(coordinate: currentLocation, addressDictionary: nil))
request.destination = MKMapItem(placemark: MKPlacemark(coordinate: destination, addressDictionary: nil))
request.transportType = .Automobile
let directions = MKDirections(request: request)
directions.calculateETAWithCompletionHandler({ ( response, error) in
if error == nil {
if let interval = response?.expectedTravelTime {
print(self.formatTimeInterval(interval))
self.durationArray.insert(self.formatTimeInterval(interval), atIndex: 0)
if self.durationArray.count == self.allUsers.count {
print(self.durationArray)
self.tableView.reloadData()
}
}
} else {
print("Error Occurred")
}
})
}
}
The issue here is that calculateETAWithCompletionHandler makes a network request. It's an asynchronous function, so you can't assume that all your calls to it will return in the same order you sent them. Here's one solution to this problem:
If you have a user object with an optional duration property, you can refactor your ETARequest method to take one as input:
func ETARequest(destination:CLLocationCoordinate2D, user: UserType) {
//Do stuff...
}
Then, in the completion handler block you pass to calculateETAWithCompletionHandler, you can replace self.durationArray.insert(self.formatTimeInterval(interval), atIndex: 0) with user.duration = self.formatTimeInterval(interval). (You can still have a counter to determine when all requests have finished.)
This way, each user will have a duration associated with it, and you can display them accordingly by looping through your users in the desired order.

How do I iterate through JSON co-ordinates and build annotations as one function?

I am struggling to get the annotations being placed using JSON data. I have tried iterating the coordinates from the JSON into a new array but when I try pass an array to where I need the coordinates it fails because it cannot take arrays. How can I fix this?
Can anyone help?
Alamofire.request(.GET, "https://demo1991046.mockable.io/score/locations").responseJSON { (responseData) -> Void in
let swiftyJsonVar = JSON(responseData.result.value!)
if let resData = swiftyJsonVar["users"].arrayObject as? [NSArray] {
self.newArray = (resData as? [NSArray])
}
print("\([self.newArray])")
for var i = 0; i < self.newArray!.count; ++i {
self.longitude.append(self.newArray[i]["lon"] as! String!)
print("longitude: \(self.longitude)")
self.latitude.append(self.newArray[i]["lat"] as! String!)
print("latitude: \(self.latitude)")
}
let doubleLat = self.latitude.map {
Double(($0 as NSString).doubleValue)
}
let doubleLon = self.longitude.map {
Double(($0 as NSString).doubleValue)
}
print("doublelat: \(doubleLat)")
print("doubleLon: \(doubleLon)")
// 1
self.locationManager.delegate = self
// 2
self.locationManager.requestAlwaysAuthorization()
// 3
let theSpan:MKCoordinateSpan = MKCoordinateSpanMake(0.01 , 0.01)
let location:CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: doubleLat, longitude: doubleLon) // <- here is where I get an error: "Cannot convert value of type '[Double]' to expect argument type 'CLLocationDegrees' (aka 'Double")"
// print("lat: \((locationManager.location?.coordinate.latitude)!)")
// print("lon: \((locationManager.location?.coordinate.longitude)!)")
let theRegion:MKCoordinateRegion = MKCoordinateRegionMake(location, theSpan)
self.mapView.setRegion(theRegion, animated: true)
let anotation = MKPointAnnotation()
anotation.coordinate = location
anotation.title = "The Location"
anotation.subtitle = "This is the location !!!"
self.mapView.addAnnotation(anotation)
}
}
I have done soem modifies below to your code
Didn't convert the json to NSArray (by using .array instead of .arrayObject)
moved adding anotation to the map inside the for loop to add all of them.
Moved setting a region to the map out side the for loop and left it to you to set the location you like.
Alamofire.request(.GET, "https://demo1991046.mockable.io/score/locations").responseJSON { (responseData) -> Void in
let swiftyJsonVar = JSON(responseData.result.value!)
// get the users from the json var, no need to convert it to Array
guard let usersJsonArray = swiftyJsonVar["users"].array else {
// users not found in the json
return
}
// the usersJsonArray is array of json which will be much easier for work with.
// No need for 1,2 and 3 to be in the for loop.
// 1
self.locationManager.delegate = self
// 2
self.locationManager.requestAlwaysAuthorization()
// 3
let theSpan:MKCoordinateSpan = MKCoordinateSpanMake(0.01 , 0.01)
for userJson in usersJsonArray {
let longitudeString = userJson["lon"].stringValue
print("longitude: \(longitudeString)")
let latitudeString = userJson["lat"].stringValue
print("latitude: \(latitudeString)")
let doubleLat = Double(latitudeString)
let doubleLon = Double(longitudeString)
print("doublelat: \(doubleLat)")
print("doubleLon: \(doubleLon)")
// by having the next code block inside the for loop you will be able to add all the user locations to the map as anotations.
let location:CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: doubleLat, longitude: doubleLon) // Now should work fine
let anotation = MKPointAnnotation()
anotation.coordinate = location
anotation.title = "The Location"
anotation.subtitle = "This is the location !!!"
self.mapView.addAnnotation(anotation)
} // for usersJson
// you need to figure out the loaction you will set for the mapView region.
let location = .... // set the location you like.
let theRegion:MKCoordinateRegion = MKCoordinateRegionMake(location, theSpan)
self.mapView.setRegion(theRegion, animated: true)
}

Resources