2

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()
eduardo
  • 123
  • 1
  • 11
  • You can have a look at https://github.com/ankitthakur/SwiftNetwork. It is supporting simultaneous http requests in swift3. – Ankit Thakur Nov 12 '16 at 06:05

1 Answers1

10

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 KVN of `isFinished` and
/// `isExecuting` for a concurrent `NSOperation` subclass. So, to developer
/// a concurrent NSOperation subclass, you instead subclass this class which:
///
/// - must override `main()` with the tasks that initiate the asynchronous task;
///
/// - must call `completeOperation()` function when the asynchronous task is done;
///
/// - optionally, periodically check `self.cancelled` status, performing any clean-up
///   necessary and then ensuring that `completeOperation()` is called; or
///   override `cancel` method, calling `super.cancel()` and then cleaning-up
///   and ensuring `completeOperation()` is called.

public class AsynchronousOperation : Operation {

    override public var isAsynchronous: Bool { return true }

    private let lock = NSLock()

    private var _executing: Bool = false
    override private(set) public var isExecuting: Bool {
        get {
            return lock.synchronize { _executing }
        }
        set {
            willChangeValue(forKey: "isExecuting")
            lock.synchronize { _executing = newValue }
            didChangeValue(forKey: "isExecuting")
        }
    }

    private var _finished: Bool = false
    override private(set) public var isFinished: Bool {
        get {
            return lock.synchronize { _finished }
        }
        set {
            willChangeValue(forKey: "isFinished")
            lock.synchronize { _finished = newValue }
            didChangeValue(forKey: "isFinished")
        }
    }

    /// Complete the operation
    ///
    /// This will result in the appropriate KVN of isFinished and isExecuting

    public func completeOperation() {
        if isExecuting {
            isExecuting = false
            isFinished = true
        }
    }

    override public func start() {
        if isCancelled {
            isFinished = true
            return
        }

        isExecuting = true

        main()
    }
}

And I use this Apple extension to NSLocking to make sure I synchronize the state changes in the above (theirs was an extension called withCriticalSection on NSLock, but this is a slightly more generalized rendition, working on anything that conforms to NSLocking and handles closures that throw errors):

extension NSLocking {

    /// Perform closure within lock.
    ///
    /// An extension to `NSLocking` to simplify executing critical code.
    ///
    /// - parameter block: The closure to be performed.

    func synchronize<T>(block: () throws -> T) rethrows -> T {
        lock()
        defer { unlock() }
        return try block()
    }
}

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.completeOperation()
        }
    }

    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.)

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Really great thanks! But don't you miss an `override public func cancel()` in `AsynchronousOperation` class where you have `isFinished = true` and call its super? Otherwise operations remain on the queue if cancelled :) – Jens Schwarzer Mar 26 '20 at 13:03
  • 1
    The docs say, “Canceling an operation does not immediately force it to stop what it is doing. ... your code must explicitly check the value in this property and abort as needed.” So, no, it wouldn’t be prudent for the default implementation of `AsynchronousOperation` to just finish the operation, but rather its subclass must decide how and when to stop the actual underlying task and only then finishing the operation. If subclass doesn’t respond to cancelation for some reason, then operation shouldn’t finish prematurely, either. – Rob Mar 26 '20 at 14:33
  • Hi Rob and thanks :) OK, so wouldn't you then have my suggested change in `NetworkOperation` instead then? Thanks for you time :) – Jens Schwarzer Mar 27 '20 at 06:30
  • 1
    The `NetworkOperation` does implement `cancel`, and it cancels the task. And when a network task is canceled, its completion handler is called (which is where we call `completeOperation`, which finishes the task). – Rob Mar 27 '20 at 06:53
  • OK I used the `completionBlock` of Operation instead of the `requestCompletionHandler` you have. Maybe this makes a difference. Sorry about that. But I added a call to `completeOperation()` in my subclass' `override func cancel()` and then no operations remained in memory. Thanks for you help :) – Jens Schwarzer Mar 27 '20 at 07:11