How to use completion handlers/dispatchqueue within while loops?
I have this method called getHub() which is a completion handler as I would like code to be executed after it has finished with the relevant values. I call this when a user presses a button:
SetupAPI().getHub(completion: { response, error in
print("testing")
print(response)
print(error)
})
(The code above is where all of the code below should end at)
It calls my API and if the API returns an error/a value that I wasn't expecting, or if Almofire couldn't do the request for some reason, then it adds one onto the tries variable. The max amount of tries allowed is 3 given by the maxTries variable. If the tries variable is equal to the maxTries variable, then a bool timeout is set to true. If the tries variable is below the maxTries variable then the code waits timeoutInSeconds - which is 10 seconds - amount of time before exiting the while loop, which should run the code once more.
Similarly, If the right value is returned from fetching the data from my API then a bool found is set to true.
If either of these variables are true, then the while loop breaks. And an error is sent back to the completion handler for the code above (which then allows me to tell the user that something has gone wrong).
However, when I run it, the completion handler above is not finished, and the code just runs through the while loop and called function over and over again as my console fills with starting and fetching via my two print statements for debugging in below code. What's the problem, can I use a DispatchQueue/ completion handlers in this situation?
Function that gets called via above code:
func getHub(completion: #escaping (Bool, Error?) -> Void) {
var tries = 0
let maxTries = 3
let timeoutInSeconds = 10.0
var found = false
var timeout = false
while !found || !timeout{
print("starting")
getHubCallAPI(completion: {status, error in
if(error == nil){
print(status)
if (status == "No documents found"){
if(tries >= maxTries){
print("Tired too many times")
timeout = true
return completion(false, nil)
}
tries += 1
DispatchQueue.main.asyncAfter(deadline: .now() + timeoutInSeconds){
return
}
}else{
found = true
print("Hub found")
return completion(true, nil)
}
}else{
print("error")
return completion(false, error)
}
})
}
}
Function that calls the API and returns it back to the function above ^^:
func getHubCallAPI(completion: #escaping (String, Error?) -> Void) {
print("fetching")
AF.request("https://discovery.ellisn.com", encoding: URLEncoding.default).response { response in
print("Request: \(response.request)")
print("Response: \(response.response)")
print("Error: \(response.error)")
if(response.error != nil){
return completion("", response.error)
}
if let data = response.data, let status = String(data: data, encoding: .utf8) {
return completion(status, nil)
}
}
}
Any questions, or more clarification needed, then just ask. Thanks.
You can try the following:
func getHub(triesLeft: Int = 3, completion: #escaping (Bool, Error?) -> Void) {
let timeoutInSeconds = 1.0
print("starting")
getHubCallAPI(completion: { status, error in
if error == nil {
print(status)
if status != "No documents found" {
print("Hub found")
return completion(true, nil)
}
} else {
print("error")
return completion(false, error) // comment out if the loop should continue on error
}
if triesLeft <= 1 {
print("Tried too many times")
return completion(false, nil)
}
DispatchQueue.main.asyncAfter(deadline: .now() + timeoutInSeconds) {
getHub(triesLeft: triesLeft - 1, completion: completion)
}
})
}
And just call it once like this:
getHub(triesLeft: 2, completion: { ... })
Note that unless you need it for some other reason, there is no need to return (Bool, Error?). And the second parameter is always nil - you may want to propagate your error. You could in theory return (String?, Error?).
Related
I have a service class that makes an api call and stores data into its property. Then my interactor class have a method where I want to make service class api call and when data will be stored - return it. I tried myself to handle this with completion handler and dispatch group, but (I suppose I just missing something) this didn't work. I would be very appreciated if you help me to deal with this problem. Thanks in advance!
Service class:
class PunkApiService{
var beers = [Beer]()
func loadList(at page: Int){
//MARK: - Checks is URL is valid + pagination
guard let url = URL(string: "https://api.punkapi.com/v2/beers?page=\(page)&per_page=25") else {
print("Invalid URL")
return
}
//MARK: - Creating URLSession DataTask
let task = URLSession.shared.dataTask(with: url){ data, response, error in
//MARK: - Handling no erros came
guard error == nil else {
print(error!)
return
}
//MARK: - Handling data came
guard let data = data else{
print("Failed to load data")
return
}
do{
let beers = try JSONDecoder().decode([Beer].self, from: data)
self.beers.append(contentsOf: beers)
}
catch{
print("Failed to decode data")
}
}
task.resume()
}
And Interactor class(without completion handler or dispatch group):
class BeersListInteractor:BeersListInteractorProtocol{
private var favoriteBeers = FavoriteBeers()
private var service = PunkApiService()
//MARK: - Load list of Beers
func loadList(at page: Int) -> [Beer]{
service.loadList(at: page)
return service.beers
}
Added: my attempt with completion handler
var beers: [Beer]
func loadList(at page: Int, completion: ()->()){
service.loadList(at: page)
completion()
}
func completion(){
beers.append(contentsOf: service.beers)
}
loadList(at: 1) {
completion()
}
This is what async/await pattern is for, described here. In your case both loadList functions are async, and the second one awaits for the first one:
class PunkApiService {
func loadList(at page: Int) async {
// change function to await for task result
let (data, error) = try await URLSession.shared.data(from: url)
let beers = try JSONDecoder().decode([Beer].self, from: data)
...
return beers
}
}
class BeersListInteractor: BeersListInteractorProtocol {
func loadList(at page: Int) async -> [Beer]{
let beers = await service.loadList(at: page)
return service.beers
}
}
See a good explanation here
I think that you were on the right path when attempting to use a completion block, just didn't do it correctly.
func loadList(at page: Int, completion: #escaping ((Error?, Bool, [Beer]?) -> Void)) {
//MARK: - Checks is URL is valid + pagination
guard let url = URL(string: "https://api.punkapi.com/v2/beers?page=\(page)&per_page=25") else {
print("Invalid URL")
completion(nil, false, nil)
return
}
//MARK: - Creating URLSession DataTask
let task = URLSession.shared.dataTask(with: url){ data, response, error in
//MARK: - Handling no erros came
if let error = error {
completion(error, false, nil)
print(error!)
return
}
//MARK: - Handling data came
guard let data = data, let beers = try? JSONDecoder().decode([Beer].self, from: data) else {
completion(nil, false, nil)
return
}
completion(nil, true, beers)
}
task.resume()
}
This is the loadList function, which now has a completion parameter that will have three parameters, respectively the optional Error, the Bool value representing success or failure of obtaining the data, and the actual [Beers] array, containing the data (if any was retrieved).
Here's how you would now call the function:
service.loadList(at: page) { error, success, beers in
if let error = error {
// Handle the error here
return
}
if success, let beers = beers {
// Data was correctly retrieved - and safely unwrapped for good measure, do what you need with it
// Example:
loader.stopLoading()
self.datasource = beers
self.tableView.reloadData()
}
}
Bear in mind the fact that the completion is being executed asynchronously, without stopping the execution of the rest of your app.
Also, you should decide wether you want to handle the error directly inside the loadList function or inside the closure, and possibly remove the Error parameter if you handle it inside the function.
The same goes for the other parameters: you can decide to only have a closure that only has a [Beer] parameter and only call the closure if the data is correctly retrieved and converted.
I am trying to recover a data set from a URL (after parsing a JSON through the parseJSON function which works correctly - I'm not attaching it in the snippet below).
The outcome returns nil - I believe it's because the closure in retrieveData function is processed asynchronously. I can't manage to have the outcome saved into targetData.
Thanks in advance for your help.
class MyClass {
var targetData:Download?
func triggerEvaluation() {
retrieveData(url: "myurl.com") { downloadedData in
self.targetData = downloadedData
}
print(targetData) // <---- Here is where I get "nil"!
}
func retrieveData(url: String, completion: #escaping (Download) -> ()) {
let myURL = URL(url)!
let mySession = URLSession(configuration: .default)
let task = mySession.dataTask(with: myURL) { [self] (data, response, error) in
if error == nil {
if let fetchedData = data {
let safeData = parseJSON(data: fetchedData)
completion(safeData)
}
} else {
//
}
}
task.resume()
}
}
Yes, it’s nil because retrieveData runs asynchronously, i.e. the data hasn’t been retrieved by the time you hit the print statement. Move the print statement (and, presumably, all of the updating of your UI) inside the closure, right where you set self.targetData).
E.g.
func retrieveData(from urlString: String, completion: #escaping (Result<Download, Error>) -> Void) {
let url = URL(urlString)!
let mySession = URLSession.shared
let task = mySession.dataTask(with: url) { [self] data, response, error in
guard
let responseData = data,
error == nil,
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode
else {
DispatchQueue.main.async {
completion(.failure(error ?? NetworkError.unknown(response, data))
}
return
}
let safeData = parseJSON(data: responseData)
DispatchQueue.main.async {
completion(.success(safeData))
}
}
task.resume()
}
Where
enum NetworkError: Error {
case unknown(URLResponse?, Data?)
}
Then the caller would:
func triggerEvaluation() {
retrieveData(from: "https://myurl.com") { result in
switch result {
case .failure(let error):
print(error)
// handle error here
case .success(let download):
self.targetData = download
// update the UI here
print(download)
}
}
// but not here
}
A few unrelated observations:
You don't want to create a new URLSession for every request. Create only one and use it for all requests, or just use shared like I did above.
Make sure every path of execution in retrieveData calls the closure. It might not be critical yet, but when we write asynchronous code, we always want to make sure that we call the closure.
To detect errors, I'd suggest the Result pattern, shown above, where it is .success or .failure, but either way you know the closure will be called.
Make sure that model updates and UI updates happen on the main queue. Often, we would have retrieveData dispatch the calling of the closure to the main queue, that way the caller is not encumbered with that. (E.g. this is what libraries like Alamofire do.)
In such a function I get a nil value but I do not understand why. The code in the middle returns an image (and I'm sure about it, I've also checked with some print statements). I do not know how is it possible that it always returns nil. It is like it ignores all the code running in the middle and considers just the first and last statement.
func getImagesDownloaded(reference: StorageReference) -> UIImage {
var imagePassedIn : UIImage?
reference.getData(maxSize: 10*1024*1024) { (data, error) in
guard let imageObject = UIImage(data: data!) else {print("Error has occurred: \(String(describing: error?.localizedDescription))"); return}
imagePassedIn = imageObject
}
if imagePassedIn == nil {
print("Error, getImagesDownloaded is not working")
}
return imagePassedIn!
}
The issue is that StorageReference.getData is an asynchronous function, but you're trying to synchronously return a value. You need to use a completion handler to return the asyncronously retrieved value.
func getImagesDownloaded(reference: StorageReference, completion: (UIImage?,Error?)->()) {
reference.getData(maxSize: 10*1024*1024) { (data, error) in
guard error == nil, let data = data else {
completion(nil,error)
return
}
guard let image = UIImage(data: data) else {
completion(nil, FirebaseErrors.expectedImage)
return
}
completion(image,nil)
}
}
enum FirebaseErrors: Error {
case expectedImage
}
Then you need to use it like this:
getImagesDownloaded(reference: yourStorageReference, completion: { image, error in
guard let image = image, error == nil else {
print(error)
return
}
// Assign your image to a UIImageView or do anything else with it inside the closure (before the ending `}`)
yourImageView.image = image
})
You are using a closure and the imageObject is probably not returned when you are doing the nil check.
Swift is executed line by line and when you have async code it executes the next line and doesn't wait for the result.
you should move the imagePassedIn check in the closure.
You're setting imagePassedIn inside a completion block. This means that when you are ready to set it, you've already retuned it.
In a few words, when using completion blocks, the code bellow won't be waiting for it to finish in order to execute.
Update your functions to this:
func getImagesDownloaded(reference: StorageReference, _ completion: (UIImage) -> ()) {
reference.getData(maxSize: 10*1024*1024) { (data, error) in
guard let data = data, let imageObject = UIImage(data: data) else {print("Error has occurred: \(String(describing: error?.localizedDescription))"); return}
completion(imageObject)
} else {
print("Error, getImagesDownloaded is not working")
}
}
I am trying to figure out how can I tell the application to wait until one task going to finish. I need to do two rest request and they depends of each other. The second one need data from first one to be successfully.
So the way I am doing my test request is :
func get(completion: #escaping (Any?) -> Void){
Alamofire.request(URL, method: .get, parameters: ["token": TOKEN]).validate().responseJSON { response in
if response.result.value != nil {
// DESERIALIZE IT INTO ARRAY " objects"
}
completion(objects)
} else {
completion(nil)
}
}
}
and the second get which needs the data from first looking simmilar like this one - only the change is the URL.
Then I would like to invoke both functions somewhere in my application and do smth with deserialized data.
func loadData() {
Service.Instance.get{ (json) in
guard json != nil else {
print("Error / json = nil pointer")
return
}
self.data = json as! [Data]
}
for dt in self.data {
Service.Instance.get(data: dt, offset: 0, limit: 10, completion: { (json2) in
guard json2 != nil else {
print("Error / json = nil pointer")
return
}
if dt.count > 0 {
self.objects = json2 as! [VehicleModel]!
self.fulldata.append(self.objects)
} else {
print("There is no object in the Array")
}
})
}
}
So I would like to tell the application to wait for first request finished before starts next cuz it will crash with nil in for loop.
This question already has answers here:
Returning data from async call in Swift function
(13 answers)
Closed 7 years ago.
My question is simple but after many research and tests I do not succeed to wait for a function end before continuing the flow.
Example :
print("Before stuff")
do_stuff {
print("After stuff")
}
func do_stuff(onCompleted: () -> ()) {
let postEndpoint: String = "http://localhost:8080/users/1234567890987654"
guard let url = NSURL(string: postEndpoint) else {
print("Error: cannot create URL")
return
}
let urlRequest = NSURLRequest(URL: url)
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
let task = session.dataTaskWithRequest(urlRequest, completionHandler: {
(data, response, error) in
guard let responseData = data else {
print("Error: did not receive data")
return
}
guard error == nil else {
print("error calling GET on /users/1234567890987654")
print(error)
return
}
// parse the result as JSON
let user: NSDictionary
do {
user = try NSJSONSerialization.JSONObjectWithData(responseData,
options: []) as! NSDictionary
} catch {
print("error trying to convert data to JSON")
// Means that user does not exist
return
}
print("The user is: " + user.description)
})
task.resume()
onCompleted()
}
How to wait do_stuff() end before the second print?
Thanks for your help, I think I miss something.
Thomas
There is something fundamental that you haven't understood. It's not actually the function, which is executing. It's the closure defined inside the function. What you need to wait is the closure to be called when the request has completed. And in fact, you should not wait, but assign an another closure outside the function to be called from the closure inside the function.
print("Before stuff")
do_stuff {
// Now the "function" has completed.
print("After stuff")
}
func do_stuff(onCompleted: () -> ()) {
let task = session.dataTaskWithRequest(urlRequest) { data, response, error in
...
onCompleted()
}
}
You could always use a completion handler for the do_stuff() function:
func do_stuff(completion: (() -> Void)?) -> () {
...
if completion != nil {
return completion!() // Return completion where you want you "after stuff" declarations to run.
}
}
And call the function as such:
do_stuff({
print("After stuff")
})