skip to Main Content

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


  1. This can be achieve using DispatchGroup and BlockOperation together like below:

    func functionWillEscapeAfter(time: DispatchTime, completion: @escaping  (Bool) -> Void) {
        
        DispatchQueue.main.asyncAfter(deadline: time) {
            completion(false) // change the value to reflect changes.
        }
        
    }
    
    func createNewUserAfterGettingResponse() {
        
        let group = DispatchGroup()
        
        let firstOperation = BlockOperation()
        firstOperation.addExecutionBlock {
            group.enter()
            print("Wait until async block returns")
            functionWillEscapeAfter(time: .now() + 5) { isSuccess in
                print("Returned value after specified seconds...")
                if isSuccess {
                    group.leave()
                    // and firstoperation will be complete
                } else {
                    firstOperation.cancel() // means first operation is cancelled and we can check later if cancelled don't execute next operation
                    group.leave()
                }
            }
            group.wait() //Waits until async closure returns something
        } // first operation ends
        
        
        let secondOperation = BlockOperation()
        secondOperation.addExecutionBlock {
            
            // Now before executing check if previous operation was cancelled we don't need to execute this operation.
            if !firstOperation.isCancelled { // First operation was successful.
                // move to next view
                moveToNextView()
                
            } else { // First operation was successful.
                // do something else.
                print("Don't move to next block")
            }
            
        }
        
        // now second operation depends upon the first operation so add dependency
        secondOperation.addDependency(firstOperation)
        
        //run operation in queue
        let operationQueue = OperationQueue()
        operationQueue.addOperations([firstOperation, secondOperation], waitUntilFinished: false)
    }
    
    func moveToNextView() {
        // move view
        print("Move to next block")
    }
    
    createNewUserAfterGettingResponse() // Call this in playground to execute all above code.
    
    

    Note: Read comments for understanding. I have run this in swift playground and working fine. copy past code in playground and have fun!!!

    Login or Signup to reply.
  2. 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 struct

    When you call the dataTask method, what happens is iOS creates a URLSessionDataTask and returns it to you. You call resume() 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, the requestNewUser method also returns. By the time your App receives the JSON response the requestNewUser 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:

    func requestNewUser(with user: User, completion: @escaping (Response?) -> String) -> String {
    

    becomes this:

    func requestNewUser(with user: User, completion: @escaping (Response?) -> Void) {
    

    And the changes to the requestNewUser looks like this:

    func requestNewUser(with user: User, completion: @escaping (Response?) -> Void) {
        //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 {
                    completion(nil)
                }
                return
            }
            let decodedResponse = try? JSONDecoder().decode(Response.self, from: data)
            DispatchQueue.main.async {
                completion(decodedResponse)
            }
        }.resume()
    }
    

    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 our createNewUserVM method from synchronous to asynchronous. We should also ask the calling code for a function that would receive the result from our Webservice class.

    So your createNewUserVM changes from this:

    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
        }
    }
    

    to this:

    func createNewUserVM(_ callback: @escaping (_ response: String?) -> Void) {
        Webservices().requestNewUser(with: User(firstName: firstName, lastName: lastName, email: email, password: password)) { serverResponse in
            guard let serverResponse = serverResponse else {
                callback("failure")
                return
            }
            callback(serverResponse.response)
        }
    }
    

    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:

    serverResponse = self.signupViewModel.createNewUserVM()
    if serverResponse == "success" {
        //move to the next view
    }
    

    To this:

    self.signupViewModel.createNewUserVM{ [weak self] (serverResponse) in
        guard let `self` = self else { return }
    
        if serverResponse == "success" {
            // move to the next view
            // self.present something...
        }
    }
    

    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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search