Search code examples
httpconcurrencyswift3nsoperationnsoperationqueue

How to make simultaneous https requests in Swift 3


I'm having problems to execute a https requests, if the request don't have any error i never get the message, this is a command line tool application and i have a plist to allow http requests, i always see the completion block.

typealias escHandler = ( URLResponse?, Data? ) -> Void

func getRequest(url : URL, _ handler : @escaping escHandler){    
let session = URLSession.shared
var request = URLRequest(url:url)
request.cachePolicy = .reloadIgnoringLocalCacheData
request.httpMethod = "GET"
let task = session.dataTask(with: url ){ (data,response,error) in
        handler(response,data)
}

task.resume()
}


func startOp(action : @escaping () -> Void) -> BlockOperation{

let exOp = BlockOperation(block: action)    
exOp.completionBlock = {

print("Finished")

}
return exOp
}

     for sUrl in textFile.components(separatedBy: "\n"){
     let url = URL(string: sUrl)!

        let queu = startOp {
            getRequest(url: url){  response, data  in

                print("REACHED")



            }

        }
      operationQueue.addOperation(queu)
      operationQueue.waitUntilAllOperationsAreFinished()

Solution

  • One problem is that your operation is merely starting the request, but because the request is performed asynchronously, the operation is immediately completing, not actually waiting for the request to finish. You don't want to complete the operation until the asynchronous request is done.

    If you want to do this with operation queues, the trick is that you must subclass Operation and perform the necessary KVO for isExecuting and isFinished. You then change isExecuting when you start the request and isFinished when you finish the request, with the associated KVO for both. This is all outlined in the Concurrency Programming Guide: Defining a Custom Operation Object, notably in the Configuring Operations for Concurrent Execution section. (Note, this guide is a little outdated (it refers to the isConcurrent property, which has been replaced is isAsynchronous; it's focusing on Objective-C; etc.), but it introduces you to the issues.

    Anyway, This is an abstract class that I use to encapsulate all of this asynchronous operation silliness:

    /// Asynchronous Operation base class
    ///
    /// This class performs all of the necessary KVO notifications of `isFinished` and
    /// `isExecuting` for a concurrent `Operation` subclass. So, to developer
    /// a concurrent `Operation` subclass, you instead subclass this class, in which the subclass must:
    ///
    /// - must override `main()` with the tasks that initiate the asynchronous task;
    ///
    /// - must call `finish()` function when the asynchronous task is done;
    ///
    /// - optionally, periodically check `self.cancelled` status, performing any clean-up
    ///   necessary and then ensuring that `finish()` is called; or
    ///   override `cancel` method, calling `super.cancel()` and then cleaning-up
    ///   and ensuring `finish()` is called.
    
    public class AsynchronousOperation: Operation {
    
        override public var isAsynchronous: Bool { true }
    
        private let lock = NSLock()
    
        private var _executing: Bool = false
        override private(set) public var isExecuting: Bool {
            get {
                lock.withLock { _executing }
            }
            set {
                willChangeValue(forKey: #keyPath(isExecuting))
                lock.withLock { _executing = newValue }
                didChangeValue(forKey: #keyPath(isExecuting))
            }
        }
    
        private var _finished: Bool = false
        override private(set) public var isFinished: Bool {
            get {
                lock.withLock { _finished }
            }
            set {
                willChangeValue(forKey: #keyPath(isFinished))
                lock.withLock { _finished = newValue }
                didChangeValue(forKey: #keyPath(isFinished))
            }
        }
    
        /// Complete the operation
        ///
        /// This will result in the appropriate KVN of isFinished and isExecuting
    
        public func finish() {
            if isExecuting {
                isExecuting = false
                isFinished = true
            }
        }
    
        override public func start() {
            if isCancelled {
                isFinished = true
                return
            }
    
            isExecuting = true
    
            main()
        }
    }
    

    Then, I can create a NetworkOperation which uses that:

    class NetworkOperation: AsynchronousOperation {
        var task: URLSessionTask!
    
        init(session: URLSession, url: URL, requestCompletionHandler: @escaping (Data?, URLResponse?, Error?) -> ()) {
            super.init()
    
            task = session.dataTask(with: url) { data, response, error in
                requestCompletionHandler(data, response, error)
                self.finish()
            }
        }
    
        override func main() {
            task.resume()
        }
    
        override func cancel() {
            task.cancel()
            super.cancel()
        }
    }
    

    Anyway, having done that, I can now create operations for network requests, e.g.:

    let queue = OperationQueue()
    queue.name = "com.domain.app.network"
    
    let url = URL(string: "http://...")!
    let operation = NetworkOperation(session: .shared, url: url) { data, response, error in
        guard let data = data, error == nil else {
            print("\(error)")
            return
        }
        
        let string = String(data: data, encoding: .utf8)
        print("\(string)")
        // do something with `data` here
    }
    
    let operation2 = BlockOperation {
        print("done")
    }
    
    operation2.addDependency(operation)
    
    queue.addOperations([operation, operation2], waitUntilFinished: false) // if you're using command line app, you'd might use `true` for `waitUntilFinished`, but with standard Cocoa apps, you generally would not
    

    Note, in the above example, I added a second operation that just printed something, making it dependent on the first operation, to illustrate that the first operation isn't completed until the network request is done.

    Obviously, you would generally never use the waitUntilAllOperationsAreFinished of your original example, nor the waitUntilFinished option of addOperations in my example. But because you're dealing with a command line app that you don't want to exit until these requests are done, this pattern is acceptable. (I only mention this for the sake of future readers who are surprised by the free-wheeling use of waitUntilFinished, which is generally inadvisable.)