I am struggling to trigger the logic responsible for changing the view at the right time. Let me explain.
I have a view model that contains a function called createNewUserVM(). This function triggers another function named requestNewUser() which sits in a struct called Webservices.
func createNewUserVM() -> String {
Webservices().requestNewUser(with: User(firstName: firstName, lastName: lastName, email: email, password: password)) { serverResponse in
guard let serverResponse = serverResponse else {
return "failure"
}
return serverResponse.response
}
}
Now that’s what’s happening in the Webservices’ struct:
struct Webservices {
func requestNewUser(with user: User, completion: @escaping (Response?) -> String) -> String {
//code that creates the desired request based on the server's URL
//...
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
DispatchQueue.main.async {
serverResponse = completion(nil)
}
return
}
let decodedResponse = try? JSONDecoder().decode(Response.self, from: data)
DispatchQueue.main.async {
serverResponse = completion(decodedResponse)
}
}.resume()
return serverResponse //last line that gets executed before the if statement
}
}
So as you can see, the escaping closure (whose code is in the view model) returns serverResponse.response (which can be either "success" or "failure"), which is then stored in the variable named serverResponse. Then, requestNewUser() returns that value. Finally, the createNewUserVM() function returns the returned String, at which point this whole logic ends.
In order to move to the next view, the idea was to simply check the returned value like so:
serverResponse = self.signupViewModel.createNewUserVM()
if serverResponse == "success" {
//move to the next view
}
However, after having written a few print statements, I found out that the if statement gets triggered way too early, around the time the escaping closure returns the value, which happens before the view model returns it. I attempted to fix the problem by using some DispatchQueue logic but nothing worked. I also tried to implement a while loop like so:
while serverResponse.isEmpty {
//fetch the data
}
//at this point, serverResponse is not empty
//move to the next view
It was to account for the async nature of the code.
I also tried was to pass the EnvironmentObject that handles the logic behind what view’s displayed directly to the view model, but still without success.
2
Answers
This can be achieve using
DispatchGroup
andBlockOperation
together like below:Note: Read comments for understanding. I have run this in swift playground and working fine. copy past code in playground and have fun!!!
As matt has pointed out, you seem to have mixed up synchronous and asynchronous flows in your code. But I believe the main issue stems from the fact that you believe
URLSession.shared.dataTask
executes synchronously. It actually executes asynchronously. Because of this, iOS won’t wait until your server response is received to execute the rest of your code.To resolve this, you need to carefully read and convert the problematic sections into asynchronous code. Since the answer is not trivial in your case, I will try my best to help you convert your code to be properly asynchronous.
1. Lets start with the
Webservices
structWhen you call the
dataTask
method, what happens is iOS creates aURLSessionDataTask
and returns it to you. You callresume()
on it, and it starts executing on a different thread asynchronously.Because it executes asynchronously, iOS doesn’t wait for it to return to continue executing the rest of your code. As soon as the
resume()
method returns, therequestNewUser
method also returns. By the time your App receives the JSON response therequestNewUser
has returned long ago.So what you need to do to pass your response back correctly, is to pass it through the "completion" function type in an asynchronous manner. We also don’t need that function to return anything – it can process the response and carry on the rest of the work.
So this method signature:
becomes this:
And the changes to the
requestNewUser
looks like this:2. View Model Changes
The
requestNewUser
method now doesn’t return anything. So we need to accommodate that change in our the rest of the code. Let’s convert ourcreateNewUserVM
method from synchronous to asynchronous. We should also ask the calling code for a function that would receive the result from ourWebservice
class.So your
createNewUserVM
changes from this:to this:
3. Moving to the next view
Now that
createNewUserVM
is also asynchronous, we also need to change how we call it from our controller.So that code changes from this:
To this:
Conclusion
I hope the answer gives you an idea of why your code didn’t work, and how you can convert any existing code of that sort to execute properly in an asynchronous fashion.