Search code examples
iosswiftgrand-central-dispatchdispatch-asyncdispatchworkitem

Synchronise multiple web service calls in serial order in swift


I am hitting a web service url 10 times and getting the response. I am using Alamofire and SwiftyJSON. This is my controller code

class ViewController: UIViewController {

    let dispatchGroup = DispatchGroup()

    var weatherServiceURL = "http://samples.openweathermap.org/data/2.5/weather?q=London,uk&appid=b6907d289e10d714a6e88b30761fae22"

    override func viewDidLoad() {
        super.viewDidLoad()
        start()
    }

    func start() {
        weatherService()
        dispatchGroup.notify(queue: .main) {
            print("All services complete")
        }
    }

    func weatherService() {
        for i in 1...10 {
            dispatchGroup.enter()
            APIManager.apiGet(serviceName: self.weatherServiceURL, parameters: ["counter":i]) { (response:JSON?, error:NSError?, count:Int) in
                if let error = error {
                    print(error.localizedDescription)
                    return
                }
                guard let response = response else { return }
                print("\n\(response) \n\(count) response\n")
                self.dispatchGroup.leave()
            }
        }
    }
}

This is my Service Handler class code

class APIManager: NSObject {

    class func apiGet(serviceName:String,parameters: [String:Any]?, completionHandler: @escaping (JSON?, NSError?, Int) -> ()) {
        Alamofire.request(serviceName, method: .get, parameters: nil, encoding: URLEncoding.default, headers: nil).responseJSON { (response:DataResponse<Any>) in

            switch(response.result) {
            case .success(_):
                if let data = response.result.value{
                    let json = JSON(data)
                    completionHandler(json,nil, parameters!["counter"] as! Int)
                }
                break

            case .failure(_):
                completionHandler(nil,response.result.error as NSError?, parameters!["counter"] as! Int)
                break
            }
        }
    }
}

I am sending a counter key with the index of for loop just to keep the track of response of which index is coming back. But the response is not coming in serial order. We can expect 3rd response before the 2nd and 1st response. This is because the API call with APIManager.apiGet function call is asynchronous and is escaping and therefore continuing the for loop.

Also I used the dispatchQueue

let dispatchQueue = DispatchQueue(label: "com.test.Queue", qos: .userInteractive)

and converted the function as:

func weatherService() {
    for i in 1...10 {
        dispatchGroup.enter()
        dispatchQueue.async {
            APIManager.apiGet(serviceName: self.weatherServiceURL, parameters: ["counter":i]) { (response:JSON?, error:NSError?, count:Int) in
                if let error = error {
                    print(error.localizedDescription)
                    return
                }
                guard let response = response else { return }
                print("\n\(response) \n\(count) response\n")
                self.dispatchGroup.leave()
            }
        }
    }
}

Same result as the service calling code is asynchronous. If we make

dispatchQueue.sync {
   //service call 
}

then also we will not get the response in serial order since the networking call in async and dispatchQueue assumes the task is completed.

Condition is to hit the service in async manner only without freezing the UI. If I hit the service is synchronous manner, then I get my desired result. But blocking main thread is not at all acceptable.

I can manage this thing using array or some global bool variables, but I don't want to use them. Is there any other way I can get response in serial order in which it is called? Any help or hint is appreciated.


Solution

  • Solution: Use DispatchSemaphores and a DispatchQueue

    Rather than saving the closures, I decided to wrap everything up in a dispatch queue and use semaphores inside it

    //Create a dispatch queue 
    let dispatchQueue = DispatchQueue(label: "myQueue", qos: .background)
    
    //Create a semaphore
    let semaphore = DispatchSemaphore(value: 0)
    
    func weatherService() {
    
        dispatchQueue.async {
            for i in 1...10 {
                APIManager.apiGet(serviceName: self.weatherServiceURL, parameters: ["counter":i]) { (response:JSON?, error:NSError?, count:Int) in
                    if let error = error {
                        print(error.localizedDescription)
                        self.semaphore.signal()
                        return
                    }
                    guard let response = response else { 
                        self.semaphore.signal()
                        return 
                    }
    
                    print("\(count) ")
    
                    //Check by index, the last service in this case
                    if i == 10 {
                        print("Services Completed")
                    } else {
                        print("An error occurred")
                    }
    
                    // Signals that the 'current' API request has completed
                    self.semaphore.signal()
                }
    
                // Wait until the previous API request completes
                self.semaphore.wait()
            }
        }
        print("Start Fetching")
    }
    

    Output is this always

    enter image description here