swift function return values not returning correctly - ios

func getLatsAndLongs() -> (LATITUDE: Double, LONGITUDE: Double, DESIREDLAT: Double, DESIREDLONG: Double) {
self.forwardGeocoding("\(addressTxtFld.text) \(cityTxtFld.text), \(stateTxtFld.text)", completion: {
success, coordinate in
if success {
self.lat = coordinate.latitude
self.long = coordinate.longitude
print("\(self.lat) is the latitude for the initial location")
print("\(self.long) is the longitude for the initial location")
self.INITIAL_DESTINATION_LATITUDE = self.lat
self.INITIAL_DESTINATION_LONGITUDE = self.long
var initialLocation = CLLocationCoordinate2DMake(self.INITIAL_DESTINATION_LATITUDE, self.INITIAL_DESTINATION_LONGITUDE)
} else {
print("Error at forwardGeocoding #willcohen #ERRORALERT")
}
})
self.forwardGeocodingDesired("\(addressTxtFldDest.text) \(cityTxtFldDest.text), \(stateTxtFldDest.text)", completion: {
success, coordinate in
if success {
self.desiredLat = coordinate.latitude
self.desiredLong = coordinate.longitude
print("\(self.desiredLat) is the latitude for the desired location")
print("\(self.desiredLong) is the longitude for the desired locaiton")
self.DESIRED_DESTIANTION_LATITUDE = self.desiredLat
self.DESIRED_DESTINATION_LONGITUDE = self.desiredLong
var desiredLocation = CLLocationCoordinate2DMake(self.DESIRED_DESTIANTION_LATITUDE, self.DESIRED_DESTINATION_LONGITUDE)
} else {
print("Error at forwardGeocodingDesired #willcohen #ERRORALERT")
}
})
return (lat,long,desiredLat,desiredLong)
}
let latsAndLongs = getLatsAndLongs()
let latFF = latsAndLongs.LATITUDE
let longFF = latsAndLongs.LONGITUDE
let latDFF = latsAndLongs.DESIREDLAT
let longDFF = latsAndLongs.DESIREDLONG
print("\(latFF) final lat")
print("\(longFF) final long")
print("\(latDFF) final latD")
print("\(longDFF) final longD")
Okay. So, when I try to print all of the lines on the last 4 lines, it comes out as "0" each time. Note, the two geocoding lines (self.forwardGeocoding) & (self.forwardGeocodingDesired) are not the problems, they work fine, but I have no idea why they are not printing the correct Double values. Any suggestions would be greatly appreciated, thank you.

forwardGeocoding and forwardGeocodingDesired invoke an operations over the network which takes time to executes. This is done using a background thread so as not to block the UI. When the operations complete the code in the completion closures is executed.
Your getLatsAndLongs function returns lat,long,desiredLat and desiredLong before the operations have completed and therefore before these variables have been set by your closure.
You can pass a completion handler to getLatsAndLongs to be invoked after the operations are complete, but your situation is complicated because you are executing two geocoding operations in parallel, and you don't know which will finish first. You could dispatch the second one from the completion handler of the first, but this will be slower. A better approach is to use a dispatch_group:
func getLatsAndLongs(completion:(initialLocation: CLLocationCoordinate2D?, desiredLocation: CLLocationCoordinate2D?)->Void) {
let dispatchGroup = dispatch_group_create()
var initialLocation: CLLocationCoordinate2D?
var desiredLocation: CLLocationCoordinate2D?
dispatch_group_enter(dispatchGroup)
self.forwardGeocoding("\(addressTxtFld.text) \(cityTxtFld.text), \(stateTxtFld.text)", completion: {
success, coordinate in
if success {
initialLocation = coordinate
} else {
print("Error at forwardGeocoding #willcohen #ERRORALERT")
}
dispatch_group_leave(dispatchGroup)
})
self.forwardGeocodingDesired("\(addressTxtFldDest.text) \(cityTxtFldDest.text), \(stateTxtFldDest.text)", completion: {
success, coordinate in
if success {
desiredLocation = coordinate
} else {
print("Error at forwardGeocodingDesired #willcohen #ERRORALERT")
}
dispatch_group_leave(dispatchGroup)
})
dispatch_group_notify(dispatchGroup, dispatch_get_main_queue()) {
completion(initialLocation:initialLocation,desiredLocation:desiredLocation)
}
}
Usage:
getLatsAndLongs { (initialLocation, desiredLocation) in
print(initialLocation)
print(desiredLocation)
}

Check if the completion blocks in self.forwardGeocoding and self.forwardGeocodingDesired execute asynchronously. If so, the values will not be set until after the prints have run.

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)")
}
}

can someone explain why i can't return a value from this method?

i'm trying to use swift geocoding to get the city, but somehow the city only showup nested inside the method and when returned the variable is empty, here is the code i'm using.
class {
var locationManager = CLLocationManager()
var longitude = CLLocationDegrees()
var latitude = CLLocationDegrees()
var city = ""
override func viewDidLoad() {
super.viewDidLoad()
setupLocation()
var x = getLocation()
print("\n\n x your city is: \(x)\n\n"); // 'x' is always empty
if x == "paris" {
print("\n\n your city is: \(x)\n\n"); // 'x' is always empty
}
}
func getLocation() -> String {
longitude = (locationManager.location?.coordinate.longitude)!
latitude = (locationManager.location?.coordinate.latitude)!
let location = CLLocation(latitude: latitude, longitude: longitude)
print(location)
CLGeocoder().reverseGeocodeLocation(location, completionHandler: {(placemarks, error) -> Void in
print(location)
if error != nil {
print("Reverse geocoder failed with error" + error!.localizedDescription)
return
}
if placemarks!.count > 0 {
let pm = placemarks![0]
print("locality is \(pm.locality)")
self.city = pm.locality!
print(" city first \(self.city)") //contains a city
}
else {
print("Problem with the data received from geocoder")
}
})
print("city second \(city)") //empty every time
return city
}
}
As pointed out here, you have to add a completion handler to your method:
func getLocation(completion: #escaping (String) -> Void) {
longitude = (locationManager.location?.coordinate.longitude)!
latitude = (locationManager.location?.coordinate.latitude)!
let location = CLLocation(latitude: latitude, longitude: longitude)
print(location)
CLGeocoder().reverseGeocodeLocation(location, completionHandler: {(placemarks, error) -> Void in
print(location)
if error != nil {
print("Reverse geocoder failed with error" + error!.localizedDescription)
return
}
if placemarks!.count > 0 {
let pm = placemarks![0]
print("locality is \(pm.locality)")
completion(pm.locality!)
}
else {
print("Problem with the data received from geocoder")
}
})
}
And then just do:
getLocation() {
locality in
self.city = locality
}
You have stumbled upon a time issue. reverseGeocodeLocation is asynchronous, so the and the method returns before the closure is fully evaluated.
If you set breakpoints you would see that the
print("city second \(city)") //empty every time
line would trigger before the
print(" city first \(self.city)") //contains a city
one
Problem:
reverseGeocodeLocation is an asynchronous method (it doesn't evaluate immediately and would take time to evaluate). Before reverseGeocodeLocation is completed getLocation will be completed.
Solution:
Modify the getLocation to accept a closure as a parameter. Inside the completion handler of reverseGeocodeLocation call that closure and pass that value of the city

BAD_EXC_ACCESS for attempting to load view controller while deallocating

I'm getting a BAD_EXC_ACCESS on line . The reason is "Attempting to load the view of a view controller while it is deallocating is not allowed and may result in undefined behavior".
func drawLocations(loc: CLLocation)
{
let center = CLLocationCoordinate2D(latitude: loc.coordinate.latitude, longitude: loc.coordinate.longitude)
let lat: CLLocationDegrees = center.latitude
let long: CLLocationDegrees = center.longitude
var points = [CLLocationCoordinate2DMake(lat,long),CLLocationCoordinate2DMake(lat,long),CLLocationCoordinate2DMake(lat,long),CLLocationCoordinate2DMake(lat,long)]
let polygon = MKPolygon(coordinates: &points, count: points.count)
mapView.addOverlay(polygon)//where I get error
}
func loadLocation(completion: (error:NSError?, records:[CKRecord]?) -> Void)
{
let query = CKQuery(recordType: "Location", predicate: NSPredicate(value: true))
CKContainer.defaultContainer().publicCloudDatabase.performQuery(query, inZoneWithID: nil){
(records, error) in
if error != nil {
print("error fetching locations: \(error)")
completion(error: error, records: nil)
} else {
print("found locations: \(records)")
print("found locations")
completion(error: nil, records: records)
guard let records = records else {
return
}
for(var i = 0; i<records.count; i += 1)
{
self.drawLocations(records[i]["location"] as! CLLocation)//where I call function
}
}
}
}
The completion block of performQuery "must be capable of running on any thread of the app" (as described in the docs). You call addOverlay which is a UI function, and so much be called on the main queue. You need to dispatch this method to the main queue.
Side note, unrelated to the question: for(var i = 0; i<records.count; i += 1) is much better written as for record in records. The C-style syntax is deprecated.

Calling asynchronous tasks one after the other in Swift (iOS)

I'm having problems with trying to run several asynchronous tasks one by one using dispatch async in swift, for an iOS weather app. I want my 'update()' function to:
get the user's location (and store the latitude & longitude in the class variables)
when location services is complete, make a call to the weather API based on newly-populated lat & long variables
when API call (and subsequent XML parsing) is complete, update the UI (an iOS table view)
(Go easy on me, I'm a recently self-taught coder, so I'm assuming the more experienced of you out there will be able to point out various errors! Any help would be massively appreciated.)
var latitude: String = ""
var longitude: String = ""
var locationManager: CLLocationManager!
var forecastData: Weather = Weather() // the weather class has it's own asynchronous NSURLSession called retrieveForecast()
// which calls the Open Weather Map API and parses the XML
func refresh() {
// Here's where I don't know what I'm doing:
let group = dispatch_group_create()
dispatch_group_enter(group)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
self.getLocation()
dispatch_group_leave(group)
}
dispatch_group_notify(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
self.getWeather()
}
self.updateUI() // ...and I know that this is in totally the wrong place!
}
// function(s) to get phone's location:
func getLocation() {
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
locationManager.requestWhenInUseAuthorization()
locationManager.distanceFilter = 100.0
locationManager.startUpdatingLocation()
}
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
self.locationManager.stopUpdatingLocation()
manager.stopUpdatingLocation()
for item: AnyObject in locations {
if let location = item as? CLLocation {
if location.horizontalAccuracy < 1000 {
manager.stopUpdatingLocation()
self.latitude = String(location.coordinate.latitude)
self.longitude = String(location.coordinate.longitude)
}
}
}
}
// function to download and parse the weather data within forecastData object
func getWeather() {
let apiCall = "http://api.openweathermap.org/data/2.5/forecast?lat=" + self.latitude
+ "&lon=" + self.longitude + "&mode=xml&appid=" + self.openWeatherMapAPIKey
NSLog("getWeather called with api request: \(apiCall)")
self.forecastData.retrieveForecast(apiCall)
}
For any asynchronous operation it is a good manner to have a finish callback.
In your case, if you have implemented callbacks for getLocation and getWeather you'll never need dispatch_groups, you just call getWeather from getLocation's callback, and then you just call refreshUI from getWeather's callback.
var latitude: String = ""
var longitude: String = ""
var locationManager: CLLocationManager!
var forecastData: Weather = Weather() // the weather class has it's own asynchronous NSURLSession called retrieveForecast()
// which calls the Open Weather Map API and parses the XML
func refresh() {
self.getLocation()
}
// function(s) to get phone's location:
func getLocation() {
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
locationManager.requestWhenInUseAuthorization()
locationManager.distanceFilter = 100.0
locationManager.startUpdatingLocation()
}
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
self.locationManager.stopUpdatingLocation()
manager.stopUpdatingLocation()
for item: AnyObject in locations {
if let location = item as? CLLocation {
if location.horizontalAccuracy < 1000 {
manager.stopUpdatingLocation()
self.latitude = String(location.coordinate.latitude)
self.longitude = String(location.coordinate.longitude)
self.getWeather()
}
}
}
}
// function to download and parse the weather data within forecastData object
func getWeather() {
let apiCall = "http://api.openweathermap.org/data/2.5/forecast?lat=" + self.latitude
+ "&lon=" + self.longitude + "&mode=xml&appid=" + self.openWeatherMapAPIKey
NSLog("getWeather called with api request: \(apiCall)")
self.forecastData.retrieveForecast(apiCall)
// assuming that self.forecastData.retrieveForecast(apiCall) is completely synchronous, we can call updateUI below
self.updateUI()
}
Here is the code, demonstrating how dispatch_group can be used right way:
func refetchOrdersAndChats(remoteNotificationData: [NSObject : AnyObject], completion: ((Bool)->())?) {
var overallSuccess: Bool = true
let refetchGroup = dispatch_group_create();
dispatch_group_enter(refetchGroup);
CPChatController.sharedInstance.updateChat(remoteNotificationData, completion: { success in
overallSuccess = success && overallSuccess
dispatch_group_leave(refetchGroup);
})
dispatch_group_enter(refetchGroup);
CPChatController.sharedInstance.fetchNewOrdersWithNotification(remoteNotificationData, completion: { success in
overallSuccess = success && overallSuccess
dispatch_group_leave(refetchGroup);
})
dispatch_group_notify(refetchGroup,dispatch_get_main_queue(), {
completion?(overallSuccess)
})
}
After going off to learn about closures/blocks, I finally found my answer so thought I'd share. What I needed to do was add a closure to the arguments in my Weather class, which returns a boolean value when the XML parsing is complete, and allows me to wait for that to happen in my View Controller before updating the UI. I hope this helps anyone else looking for a similar answer, and if any pros are able to make this code even better, please do add a comment!
...
// getWeather function in my View Controller (called from location manager)
func getWeather() {
let apiCall = "http://api.openweathermap.org/data/2.5/forecast?lat=" + self.latitude
+ "&lon=" + self.longitude + "&mode=xml&appid=" + self.openWeatherMapAPIKey
NSLog("getWeather called with api request: \(apiCall)")
self.forecastData.retrieveForecast(apiCall, completion: { success in
if success {
self.updateUI()
} else {
NSLog("Parse unsuccessful")
// I'll handle the issue here
}
});
}
...And in the Weather class function:
func retrieveForecast(url: String, completion: ((Bool)->())?) {
self.reset()
let apiCall = NSURL(string: url)
let urlRequest: NSURLRequest = NSURLRequest(URL: apiCall!)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(urlRequest) {
(data, response, error) -> Void in
self.xmlParser = NSXMLParser(data: data!)
self.xmlParser.delegate = self
let success: Bool = self.xmlParser.parse()
if !success {
NSLog("Did not parse. \(error?.localizedDescription)")
completion?(false)
} else {
self.currentParsedElement = ""
self.retrievingForecast = false
completion?(true)
}
}
task.resume()
}

How to set properties within a Closure/Block in Swift

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.

Resources