-1

I am trying to get a string out from an external JSON file, which is on a web server, and it gets it successfully, but it is inside a closure where it gets the value, and I need to get it outside so I can return it with the variable returnip How do I do this?

func getJsonFromUrl() -> String {

    let URL2 = "https://url.com/asd.php";
    let url = URL(string: URL2)
    URLSession.shared.dataTask(with:url!) { (data, response, error) in
        if error != nil {
            print(error as Any)
        } else {
            do {

                let parsedData = try JSONSerialization.jsonObject(with: data!) as! [String:Any]
                let ips = parsedData["ip"] as! String
                print("The IP is: " + ips) //Prints the value correctly
               var returnip = ips //The value that I want to return, that does not go outside this closure
            } catch let error as NSError {
                print(error)
            }
        }

        }.resume()
     return returnip //Does not return anything
}

Thanks

rmaddy
  • 314,917
  • 42
  • 532
  • 579
FrankFabregat
  • 81
  • 2
  • 11
  • You cannot do it this way. Web calls are asynchronous. The return will happen before the URL call completes. – ryantxr Sep 04 '17 at 18:20

1 Answers1

1

You cannot return from an asynchronous function and a return statement inside a closure only returns from the closure itself.

You need to use a completion handler instead. Also, don't use force unwrapping of optionals/force casting optionals when parsing a network response.

func getJsonFromUrl(name: String, completion: @escaping (String?)->()) {
    //use name variable just as you would in a normal function
    let URL2 = "https://url.com/asd.php"
    let url = URL(string: URL2)
    URLSession.shared.dataTask(with:url!) { (data, response, error) in
        if error != nil {
            print(error as Any)
            completion(nil)
        } else {
            do {
                guard let parsedData = try JSONSerialization.jsonObject(with: data!) as? [String:Any] else { completion(nil); return }
                guard let ips = parsedData["ip"] as? String else {completion(nil); return }
                print("The IP is: " + ips) //Prints the value correctly
                completion(ips)
            } catch let error as NSError {
                print(error)
                completion(nil)
            }
        }
    }.resume()
}

Then you can call it like this:

getJsonFromUrl(name: "Input", completion: { ips in
    print(ips)
    //you can only use the value inside the closure of completion
})
Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
  • What would I do if the function has an argument? Example, `func getJsonFromUrl(name: String, completion: @escaping (String?)->()) {}`. Is that the way to make the function, and how do you call it? – FrankFabregat Sep 04 '17 at 18:26
  • BTW, `dataTask(with: URL)` will be running in background queue. So, if you need to do work related to UI, please do it in main queue. – antonio081014 Sep 04 '17 at 18:32
  • By the way, I need to call it as a variable, so 'let variable = getJsonFromUrl(name: name, completion: { ips in print(ips) })'. Is that correct? – FrankFabregat Sep 04 '17 at 18:32
  • @antonio081014 ok – FrankFabregat Sep 04 '17 at 18:33
  • @FrankFabregat the information you need, String will be returned as a parameter in your closure. – antonio081014 Sep 04 '17 at 18:35
  • @FrankFabregat no, the function has a Void return value, so you can't assign it to a variable. My answer has all the code needed for you to make it work. Check my updated answer if you need to have an input argument. – Dávid Pásztor Sep 04 '17 at 18:37
  • @DávidPásztor One more question: Is it possible to set ips as a global variable when calling it? So: `getJsonFromUrl(name: "Input", completion: { ips in print(ips); ip2 = ips! })`. ips is specified outside the ViewController as `var ip2 = ""`. Thank you very much :) – FrankFabregat Sep 04 '17 at 18:50
  • You can set assign it to a variable in another scope (however, the use of global variables is discouraged in most cases, for sharing data between viewcontrollers especially there are better ways, just search it here on SO). The main problem is, that you might still be accessing the global variable before the asynchronous function would finish execution. It is better to use it only inside the closure or use `DispatchGroup`s to wait for the function to finish execution. – Dávid Pásztor Sep 04 '17 at 18:58
  • @DávidPásztor But is it possible to put it on a global variable, and how? – FrankFabregat Sep 04 '17 at 19:35
  • Just as you wrote in your comment. But don't force unwrap `ips`, it is optional for a reason. Use optional binding to safely unwrap it. – Dávid Pásztor Sep 04 '17 at 21:07
  • @DávidPásztor It's not setting it as a global variable... – FrankFabregat Sep 04 '17 at 22:06
  • Did you define it outside the class scope? More importantly, are you sure you are checking the value of `ip2`, __after__ it's been set? As I've told you, you will most probably end up accessing `ip2` before the completion handler returned, hence you shouldn't save `ips` to a variable outside the closure's scope in the first place. – Dávid Pásztor Sep 04 '17 at 22:12
  • @DávidPásztor I am indeed declaring the variable outside the ViewController and I am checking the value after setting it equal to ip2. – FrankFabregat Sep 05 '17 at 01:43
  • How do you make sure you check it after the closure is executed? The completion handler is executed asynchronously, so just writing the code to use the variable after the closure ensures nothing. – Dávid Pásztor Sep 05 '17 at 08:48