0

How can I return a value within an if let statement to be further returned within a function? Here is the code:

func loadUrl(url:String) -> String {
        DispatchQueue.global().async {
            do {
                let appUrl = URL(string:url)!
                let data = try Data(contentsOf:appUrl)
                let json = try JSONSerialization.jsonObject(with: data) as! [String:Any]
                print("Test from do")
                if let results = json["results"] as? [[String:Any]] {
                    print("Test from if let 1")
                    if let first = results[0] as? [String:Any] {

                        print("Test from if let 2")
                        var cityStateLocation = first["formatted_address"]!

                        return cityStateLocation
                        //What needs to be returned
                    }
                }
                DispatchQueue.main.async {
                    print("No Error")
                }
            } catch {
                DispatchQueue.main.async {
                    print("Cannot connect to the server.")
                }
            }
        }
    }

What I would like to be able to do is take cityStateLocation and return it in the func, but because it is a part of an if let statement within an .async method I don't know how to do that. Could someone please explain?

EDIT: I need the return value of cityStateLocation to equal a variable in a separate function. Here is the separate function:

@IBAction func continueButton(_ sender: Any) {
        var cityState:String
        if locationSwitch.isOn == true {
            print(location.latitude)
            print(location.longitude)
            let url = "https://maps.googleapis.com/maps/api/geocode/json?latlng=\(location.latitude),\(location.longitude)&result_type=locality&key=AIzaSyDI-ZacHyPbLchRhkoaUTDokwj--z_a_jk"
            loadUrl(url: url)
            cityState = loadUrl(url: url)
        } else {
            cityState = ""
        }
        CoreDataHandler.saveObject(locationLocality: cityState)
    }

Edit 2: The main reason why the "duplicate answer" is not a duplicate is that my code needs to call the return of this function within a separate function then save it to Core Data. Also, my code is not using an array.

Patrick Haertel
  • 309
  • 4
  • 15
  • Use a closure instead - after the call is dispatched to the `DispatchQueue`, the function will return immediately, usually before what you dispatched is run – MadProgrammer Jun 01 '18 at 01:47
  • The problem is not the `if let`, it's the `async` call. The point of `async` is that your function will move on before the closure has executed, so you can't return anything from it. You'd need to have `loadUrl` accept a closure in addition to `url`, and call that closure with `cityStateLocation`. – zneak Jun 01 '18 at 01:49
  • How would I create a closure for this statement? Doesn't that mean it has to be an array? – Patrick Haertel Jun 01 '18 at 01:53
  • **Never** use synchronous `Data(contentsOf` API to load data from a remote URL not even in an asynchronous dispatch queue. – vadian Jun 01 '18 at 15:22

3 Answers3

4

You could modify your function to include a closure. For instance:

func loadUrl(url: String, completionHandler: @escaping (_ location: String?) -> (Void)) {

And then, where you want to return it, you'd pass it in as such.

completionHandler(cityStateLocation)

I made it an optional so that, in your fail paths, you could return nil.

Then, where you call the function would change. Using trailing closure syntax, it could look like this:

loadUrl(url: "someurl.com/filepath.txt") { optionalLocation in 
    guard let nonOptionalLocation = optionalLocation else {
        // Location was nil; Handle error case here
        return
    }
    // Do something with your location here, like setting UI or something
}

This is a fairly common pattern when dealing with asynchronous activity, such as working with network calls.

Josh Hrach
  • 1,238
  • 8
  • 15
  • Now, how would I set a variable in a separate func to be equal to the value of the completion handler which is cityStateLocation? Before I had the code as cityState = loadUrl(url: url). Can still set a var to be equal to the loadUrl func and get the value of cityStateLocation. Basically cityStateLocation is being read from JSON and then when a save button is pressed cityStateLocation is saved to Core Data. – Patrick Haertel Jun 01 '18 at 02:05
  • 1
    Below the guard, you could set your variable. So if it's a cityState property stored on your class, you could simply do it as `self.cityState = nonOptionalLocation` – Josh Hrach Jun 01 '18 at 02:35
  • 1
    Once you're below that guard statement, you can do whatever you want with that value. You can pass it into another function to save it to CoreData, for instance. – Josh Hrach Jun 01 '18 at 02:37
  • so would the code be: guard let nonOptionalLocation = optionalLocation else { return } cityState = nonOptionalLocation } — this gives an error Variable ‘cityState’ captured by a closure before being initialized – Patrick Haertel Jun 01 '18 at 02:57
  • The variable cityState is defined within the same function. So self. is not needed. – Patrick Haertel Jun 01 '18 at 02:59
  • Your error is saying the variable wasn't initialized. You probably just have `var cityState: String` without setting it to a value first. Setting it to an empty string should fix your error. `var cityState: String = ""` – Josh Hrach Jun 01 '18 at 15:27
  • Yeah. I figured that out. Now I’m just having a problem with the order that things are happening. See my latest question. – Patrick Haertel Jun 01 '18 at 15:33
  • In your if statement, where you call `loadURL()`, check it to: ` loadURL(url: url) { location in if let location = location { CoreDataHandler.saveObject(locationLocality: location) } }` That will trigger the save after returning from loadURL. (Sorry, comments don't keep multi line formatting, so I hope you can decipher that.) – Josh Hrach Jun 01 '18 at 21:54
0

The simplest (perhaps no the prettiest), way of doing this would simply be to declare and instantiate a variable above the dispatch queue. Then you can set the variable equal to whatever you want, within the dispatch queue, and return it afterwards. You can change the type of ret, so that it suits your needs more directly.

func loadUrl(url:String) -> String {
    var ret = NSObject()
    DispatchQueue.global().async {
        do {
            let appUrl = URL(string:url)!
            let data = try Data(contentsOf:appUrl)
            let json = try JSONSerialization.jsonObject(with: data) as! [String:Any]
            print("Test from do")
            if let results = json["results"] as? [[String:Any]] {
                print("Test from if let 1")
                if let first = results[0] as? [String:Any] {

                    print("Test from if let 2")
                    var cityStateLocation = first["formatted_address"]!

                    ret = cityStateLocation
                    //What needs to be returned
                }
            }
            DispatchQueue.main.async {
                print("No Error")
            }
        } catch {
            DispatchQueue.main.async {
                print("Cannot connect to the server.")
            }
        }
    }
return ret
}
Gabe Spound
  • 568
  • 5
  • 28
0

DispatchQueue.global().async will cause the coded included in the closure to be executed at some point the future, meaning you loadUrl function will return (almost) immediately.

What you need is some kind of callback which can be called when you have a result (AKA closure)

This is just another way to approach the problem, the difference between this and Josh's example is simply, I provide an additional closure to handle the errors

func loadUrl(url:String, complition: @escaping (String?) -> Void, fail: @escaping (Error) -> Void) {
    DispatchQueue.global().async {
        do {
            let appUrl = URL(string:url)!
            let data = try Data(contentsOf:appUrl)
            let json = try JSONSerialization.jsonObject(with: data) as! [String:Any]
            print("Test from do")
            if let results = json["results"] as? [[String:Any]], !results.isEmpty {
                print("Test from if let 1")
                let first = results[0]

                print("Test from if let 2")
                if let cityStateLocation = first["formatted_address"] as? String {
                    complition(cityStateLocation)
                } else {
                    complition(nil)
                }
            } else {
                complition(nil)
            }
        } catch let error {
            fail(error)
        }
    }
}

Which you might call using something like...

loadUrl(url: "your awesome url", complition: { (value) in
    guard let value = value else {
        // No value
        return
    }
    // process value
}) { (error) in
    // Handle error
}
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366