I have code where I am creating a URLSession
and trying to return the data obtained from this so it can be used outside of the function that the URLSession
is in. My code looks like:
var nameStr = ""
var ingStr = ""
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
guard let data = data else { return }
let str = String(data: data, encoding: .utf8)!
let str2 = convertStringToDictionary(text: str)
nameStr = "Name: \(str2!["title"] as! NSString)"
ingStr = "Ingredients: \((str2!["ingredientList"] as! NSString).components(separatedBy: ", "))"
print("First ", nameStr)
print(ingStr)
}
task.resume()
print("Second ", nameStr)
print(ingStr)
The first print statement works as expected, but the second only prints "Second ". I looked at Swift return data from URLSession and although I realize it does have a viable solution, I do not understand the whole completion block part and have a feeling others might be on the same boat. Any suggestions?
You said:
The first print statement works as expected, but the second only prints "Second ". I looked at Swift return data from URLSession …
The accepted answer to that question is the correct answer here, too.
What you’ve labeled as “first” and “second” in your code snippet are actually backwards. If you look carefully, you will see that “second” print statement (with no results yet) is actually happening before you see your “first” print statement! More to the point, your execution path will reach that “second” print statement before you’ve received and parsed the response from the network call.
That is the root of the issue, namely that the code inside the closure passed to dataTask
method happens “asynchronously”, i.e. later. You can’t print
your strings right after the resume
statement (your “second” print statement) because those strings have not been set yet! Likewise, obviously cannot return
these strings, either, because, again, they are not populated until well after you’ve returned from this function. This is all because the dataTask
closure has not run yet! The completion handler closure, described in that other answer, is the solution.
Consider this code snippet:
let task = URLSession.shared.dataTask(with: url) { data, _, _ in
guard
let data = data,
let string = String(data: data, encoding: .utf8),
let dictionary = convertStringToDictionary(text: string),
let title = dictionary["title"] as? String,
let ingredientsString = dictionary["ingredientList"] as? String
else { return }
let ingredients = ingredientsString.components(separatedBy: ", ")
// you’ve got `title` and `ingredients` here …
}
task.resume()
// … but not here, because the above runs asynchronously
This is basically your code, refactored to eliminate the !
forced unwrapping operators and the NSString
references. But that’s not the relevant piece of the story. It is the those two comments, where I’m showing where you have results (i.e., in the closure) and where you do not (i.e., immediately after the resume
). If you’re not familiar with asynchronous programming, this might look deeply confusing. But it is the important lesson, to understand exactly what “asynchronous” means.
… but I do not understand the whole completion block part. Any suggestions?
Yep, the answer to your question rests in understanding this “completion handler” pattern. The dataTask
method has a completion handler closure parameter, a pattern that you have to repeat in your own method.
So, going back to my code snippet above, how precisely do you “return” something that is retrieved later? The answer: You technically do not return
anything. Instead, add a completion handler closure parameter to your method and call that method with a Result
, which is either some model object upon success (e.g., perhaps a Recipe
object), or an Error
upon failure. And to keep it simple for the caller, we will call that completion handler on the main queue.
Thus, perhaps:
func fetchRecipe(with url: URL, completion: @escaping (Result<Recipe, Error>) -> Void) {
let task = URLSession.shared.dataTask(with: url) { data, _, _ in
guard
let data = data,
let string = String(data: data, encoding: .utf8),
let dictionary = convertStringToDictionary(text: string),
let title = dictionary["title"] as? String,
let ingredientsString = dictionary["ingredientList"] as? String
else {
DispatchQueue.main.async {
completion(.failure(error ?? URLError(.badServerResponse)))
}
return
}
let ingredients = ingredientsString.components(separatedBy: ", ")
let recipe = Recipe(title: title, ingredients: ingredients)
DispatchQueue.main.async {
completion(.success(recipe))
}
}
task.resume()
}
This is basically the same as my above code snippet, except that I:
fetchRecipe
method;return
anything;completion
parameter, which is a completion handler closure;completion
closure when the network request completes, passing back either a .failure()
or a .success()
, obviously depending upon whether it failed or succeeded.For the sake of completeness, this is the Recipe
model object that I used, to wrap everything returned from the server in a nice, simple object:
struct Recipe {
let title: String
let ingredients: [String]
}
And you would call it like so:
fetchRecipe(with: url) { result in
// use `result` here …
switch result {
case .failure(let error):
print(error)
case .success(let recipe):
print(recipe)
// update your model and UI using `result` here …
}
}
// … but not here, because, again, the above runs asynchronously
I have to say, it feels deeply wrong to convert the Data
to a String
and then to a dictionary and then use dictionary key string literals. Usually, for example, we’d have a server that returned JSON and we would just parse the Data
directly with JSONDecoder
. But you did not share this convertStringToDictionary
, so I wasn’t able to offer any meaningful advice on that score. But that’s something for you to tackle once you have your immediate problem behind you.