iosswiftpostgrand-central-dispatch

Is there an method to stop the flow until I get response from API in Swift?


Basically, we do have an APIHelper class where GET and POST methods are implemented, that can be called from any of the View Controllers, we will be sending an access token for security in the header, once that access token is expired we need to call an API for getting access token and need to send the updated token in the header.

class func postMethod(methodName: String, success: @escaping (AnyObject?, String?)->Void, Failure:@escaping (NSError)->Void)
{
    do {
        
        if ReachabilityManager.shared.isConnectedToNetwork() == false
        {
            PageNavigation.moveToInternet()
            
            
        } else {
            
            let session = URLSession.shared
            let urlPath = URL(string: "\(AppConstants.BaseUrl)" + "\(methodName)")
            var request = URLRequest(url: urlPath! as URL)
            
            print( "JSON request is \(urlPath!)")
            request.httpMethod = "POST"
            
            request = WebAPIHelper.headers(methodType: "POST", methodName: methodName, request: request, isAddUserAuthorization: false)
            
            let jsonBody = try JSONEncoder().encode(CommonDBHelper.getActiveUserRefreshToken())
            request.httpBody = jsonBody
            
            let task = session.dataTask(with: request, completionHandler: {(data, response, error) in
                if(error == nil)
                {
                    do
                    {
                        if let httpResponse = response as? HTTPURLResponse
                        {
                            let headers = httpResponse.allHeaderFields
                            let statusCode = httpResponse.statusCode
                            
                             if (statusCode == 200)
                            {
                                let JSON = try JSONSerialization.jsonObject(with: data!, options:JSONSerialization.ReadingOptions.allowFragments)
                                
                                if let currentServerTime = headers["Date"] as? String
                                {
                                    success(JSON as AnyObject?, currentServerTime)
                                }
                                else{
                                    success(JSON as AnyObject?, nil)     // Closure being called as a function
                                }
                            }
                            
                            else
                            {
                                GenericMethods.showAlert(alertMessage: "Some error occured. Please try again later")
                            }
                        }
                        else
                        {
                            GenericMethods.showAlert(alertMessage: "Some error occured. Please try again later")
                        }
                    }
                    catch let JSONError as NSError
                    {
                        Failure(JSONError as NSError)
                        
                        print("JSON Error \(JSONError)")
                        GenericMethods.showAlert(alertMessage: "Some error occured. Please try again later")
                    }
                }
                else
                {
                    Failure(error! as NSError)
                    GenericMethods.showAlert(alertMessage: "Some error occured. Please try again later")
                }
            })
            task.resume()
        }
    }
    catch {
        print("Error in catch \(error.localizedDescription)")
    }
    
}

So GET and POST will be called by many threads at a time, so how to keep all the calls in waiting mode till I get a response from access token API. Is there any solution for this type of Scenario?

class func headers(methodType:String, methodName:String, request:URLRequest, isAddUserAuthorization : Bool) -> URLRequest
{
    let methodName = methodName
    var request = request
    
    


        if isTokenExpired(){
            
            //how to keep waiting all other api calling in waiting mode here until i get response
            
            RefreshTokenAPI(completed: { (accesstoken) ->Void in
                request.addValue(accesstoken, forHTTPHeaderField: "AccessToken")
            })
            
        }
        else{
            request.addValue(getAccessTokenFromDefaults(), forHTTPHeaderField: "AccessToken")
        }
        
    
    
   
    request.addValue("application/json", forHTTPHeaderField: "Content-Type")
    request.addValue("application/json", forHTTPHeaderField: "Accept")
    
    
    
    print(request.allHTTPHeaderFields!)
    
    return request as URLRequest
}

Solution

  • The goal here is to initiate the subsequent request after you have received the access token. One would generally employ a completion handler pattern, e.g. a closure that is called only after the access request is retrieved:

    class func headers(methodType: String, methodName: String, request: URLRequest, isAddUserAuthorization: Bool, completion: @escaping (URLRequest) -> Void) {
        var request = request
    
        // presumably you have code that is using methodType and methodName, too ...
    
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
        request.addValue("application/json", forHTTPHeaderField: "Accept")
    
        if isTokenExpired() { 
            refreshTokenAPI { accesstoken in
                request.addValue(accesstoken, forHTTPHeaderField: "AccessToken")
                completion(request)
            }
        } else {
            request.addValue(getAccessTokenFromDefaults(), forHTTPHeaderField: "AccessToken")
            completion(request)
        }
    }
    

    Then rather than:

    request = WebAPIHelper.headers(methodType: "POST", methodName: methodName, request: request, isAddUserAuthorization: false)
    
    ...
    
    // currently issuing request here
    

    The caller would move everything dependent upon the request in the headers completion handler:

    WebAPIHelper.headers(methodType: "POST", methodName: methodName, request: request, isAddUserAuthorization: false) { request in
        // issue request here
        ...
    }
    
    // but not here
    

    This is the simple “initiate request once access token is retrieved” pattern.

    There are other patterns you could pursue, too. E.g., you could create a queue for authorized requests which is suspended until RefreshTokenApi resumes that queue. Then, all requests that require the token would be added that queue. Or you could use custom Operation pattern, where the isReady is set when the token is successfully retrieved. There are lots of ways to skin the cat, but hopefully this illustrates the idea, that we employ asynchronous patterns rather than ever “stopping” or “waiting”.


    If you want to make sure that you allow concurrent requests to call refreshTokenAPI, then you can have it wait, but only do this on some background queue, and avoid ever blocking the caller thread:

    private var queue = DispatchQueue(label: "com.domain.app.token") // custom serial queue to avoid blocking calling thread
    
    func headers(methodType: String, methodName: String, request: URLRequest, isAddUserAuthorization: Bool, completion: @escaping (URLRequest) -> Void) {
        queue.async { [self] in
            var request = request
    
            // presumably you have code that is using methodType and methodName, too ...
    
            request.addValue("application/json", forHTTPHeaderField: "Content-Type")
            request.addValue("application/json", forHTTPHeaderField: "Accept")
    
            if isTokenExpired() {
                let semaphore = DispatchSemaphore(value: 0)
    
                refreshTokenAPI { token in
                    request.addValue(token, forHTTPHeaderField: "AccessToken")
                    DispatchQueue.main.async { completion(request) }
                    semaphore.signal()
                }
                semaphore.wait()
            } else {
                request.addValue(getAccessTokenFromDefaults(), forHTTPHeaderField: "AccessToken")
                DispatchQueue.main.async { completion(request) }
            }
        }
    }
    

    Frankly, I might consider wrapping these network requests in a custom asynchronous subclass of Operation (or use Combine), but that is beyond the scope of this question.