iosswiftswift2nsoperationnsoperationqueue

[NSBlockOperation addExecutionBlock:]: blocks cannot be added after the operation has started executing or finished


I am trying to start again NSBlockOperation after completing or canceling it, but getting an error? Any one have any idea where is mistake? Thanks

let imageURLs = ["http://www.planetware.com/photos-large/F/france-paris-eiffel-tower.jpg",
    "http://adriatic-lines.com/wp-content/uploads/2015/04/canal-of-Venice.jpg",
    "http://algoos.com/wp-content/uploads/2015/08/ireland-02.jpg",
    "http://bdo.se/wp-content/uploads/2014/01/Stockholm1.jpg"]

class Downloader {

    class func downloadImageWithURL(url:String) -> UIImage! {

        let data = NSData(contentsOfURL: NSURL(string: url)!)
        return UIImage(data: data!)
    }
}

class ViewController: UIViewController {

    @IBOutlet weak var imageView1: UIImageView!
    var indeX = 0

    let operation1 = NSBlockOperation()
    var queue = NSOperationQueue()

    override func viewDidLoad() {
        super.viewDidLoad()
    }
    @IBAction func didClickOnStart(sender: AnyObject) {
        queue = NSOperationQueue()

        operation1.addExecutionBlock { () -> Void in

            for _ in imageURLs {
                if !self.operation1.cancelled {
                    let img1 = Downloader.downloadImageWithURL(imageURLs[self.indeX])
                    NSOperationQueue.mainQueue().addOperationWithBlock({
                        self.imageView1.image = img1

                        print("indeX \(self.indeX)")
                        self.indeX++
                    })

                }
            }
        }
        queue.addOperation(operation1)
    }

    @IBAction func didClickOnCancel(sender: AnyObject) {
        self.queue.cancelAllOperations()
        print(operation1.finished)
    }
}  

Output

indeX 0
false
indeX 1
2016-07-20 02:00:26.157 ConcurrencyDemo[707:15846] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSBlockOperation addExecutionBlock:]: blocks cannot be added after the operation has started executing or finished'
*** First throw call stack:
(
    0   CoreFoundation                      0x000000010c94be65 __exceptionPreprocess + 165
    1   libobjc.A.dylib                     0x000000010e68bdeb objc_exception_throw + 48
    2   Foundation                          0x000000010cd369fe -[NSBlockOperation addExecutionBlock:] + 356
    3   ConcurrencyDemo                     0x000000010c766edd _TFC15ConcurrencyDemo14ViewController15didClickOnStartfS0_FPSs9AnyObject_T_ + 253
    4   ConcurrencyDemo                     0x000000010c767086 _TToFC15ConcurrencyDemo14ViewController15didClickOnStartfS0_FPSs9AnyObject_T_ + 54
    5   UIKit                               0x000000010d16a194 -[UIApplication sendAction:to:from:forEvent:] + 92
    6   UIKit                               0x000000010d56b7b7 -[UIBarButtonItem(UIInternal) _sendAction:withEvent:] + 152
    7   UIKit                               0x000000010d16a194 -[UIApplication sendAction:to:from:forEvent:] + 92
    8   UIKit                               0x000000010d2d96fc -[UIControl sendAction:to:forEvent:] + 67
    9   UIKit                               0x000000010d2d99c8 -[UIControl _sendActionsForEvents:withEvent:] + 311
    10  UIKit                               0x000000010d2d9b43 -[UIControl _sendActionsForEvents:withEvent:] + 690
    11  UIKit                               0x000000010d2d8af8 -[UIControl touchesEnded:withEvent:] + 601
    12  UIKit                               0x000000010d1d949b -[UIWindow _sendTouchesForEvent:] + 835
    13  UIKit                               0x000000010d1da1d0 -[UIWindow sendEvent:] + 865
    14  UIKit                               0x000000010d188b66 -[UIApplication sendEvent:] + 263
    15  UIKit                               0x000000010d162d97 _UIApplicationHandleEventQueue + 6844
    16  CoreFoundation                      0x000000010c877a31 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
    17  CoreFoundation                      0x000000010c86d95c __CFRunLoopDoSources0 + 556
    18  CoreFoundation                      0x000000010c86ce13 __CFRunLoopRun + 867
    19  CoreFoundation                      0x000000010c86c828 CFRunLoopRunSpecific + 488
    20  GraphicsServices                    0x0000000110f5ead2 GSEventRunModal + 161
    21  UIKit                               0x000000010d168610 UIApplicationMain + 171
    22  ConcurrencyDemo                     0x000000010c76906d main + 109
    23  libdyld.dylib                       0x000000010f19492d start + 1
    24  ???                                 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
(lldb)

Solution

  • Let me outline a series of alternatives. The first is just addressing the immediate tactical problem in your question, and the latter two being further refinements, of increasing complexity.

    1. You cannot call addExecutionBlock once an operation has started. So just create a new Operation.

      For example:

      class ViewController: UIViewController {
      
          @IBOutlet weak var imageView1: UIImageView!
      
          weak var downloadOperation: Operation?    // make this weak
      
          var queue = OperationQueue()
      
          let imageURLs: [String] = ...
      
          @IBAction func didClickOnStart(_ sender: Any) {
              downloadOperation?.cancel()             // you might want to stop the previous one if you restart this
      
              let operation = BlockOperation {
                  for (index, imageURL) in self.imageURLs.enumerate() {
                      guard let cancelled = self.downloadOperation?.cancelled where !cancelled else  { return }
      
                      let img1 = Downloader.downloadImageWithURL(imageURL)
                      OperationQueue.main.addOperation {
                          self.imageView1.image = img1
      
                          print("index \(index)")
                      }
                  }
              }
              queue.addOperation(operation)
      
              downloadOperation = operation
          }
      
          @IBAction func didClickOnCancel(_ sender: Any) {
              downloadOperation?.cancel()
          }
      
      }
      
    2. It's worth noting that this is going to be unnecessarily slow, loading the images consecutively. You could load them concurrently with something like:

      class ViewController: UIViewController {
      
          @IBOutlet weak var imageView1: UIImageView!
      
          var queue: OperationQueue = {
              let _queue = OperationQueue()
              _queue.maxConcurrentOperationCount = 4
              return _queue
          }()
      
          let imageURLs: [String] = ...
      
          @IBAction func didClickOnStart(_ sender: Any) {
              queue.cancelAllOperations()
      
              let completionOperation = BlockOperation {
                  print("all done")
              }
      
              for (index, imageURL) in self.imageURLs.enumerate() {
                  let operation = BlockOperation {
                      let img1 = Downloader.downloadImageWithURL(imageURL)
                      OperationQueue.main.addOperation {
                          self.imageView1.image = img1
      
                          print("index \(index)")
                      }
                  }
                  completionOperation.addDependency(operation)
                  queue.addOperation(operation)
              }
      
              OperationQueue.main.addOperation(completionOperation)
          }
      
          @IBAction func didClickOnCancel(_ sender: Any) {
              queue.cancelAllOperations()
          }
      }
      
    3. Even that has issues. The other problem is that when you “cancel”, it may well continue trying to download the resource currently being downloaded because you're not using a cancelable network request.

      An even better approach would be to wrap the download (which is to be performed via URLSession) in its own asynchronous Operation subclass, and make it cancelable, e.g.:

      class ViewController: UIViewController {
          var queue: OperationQueue = {
              let _queue = OperationQueue()
              _queue.maxConcurrentOperationCount = 4
              return _queue
          }()
      
          let imageURLs: [URL] = ...
      
          @IBAction func didClickOnStart(_ sender: Any) {
              queue.cancelAllOperations()
      
              let completion = BlockOperation {
                  print("done")
              }
      
              for url in imageURLs {
                  let operation = ImageDownloadOperation(url: url) { result in
                      switch result {
                      case .failure(let error): 
                          print(url.lastPathComponent, error)
      
                      case .success(let image): 
                          OperationQueue.main.addOperation {
                              self.imageView1.image = img1
      
                              print("index \(index)")
                          }
                      }
                  }
                  completion.addDependency(operation)
                  queue.addOperation(operation)
              }
      
              OperationQueue.main.addOperation(completion)
          }
      
          @IBAction func didClickOnCancel(_ sender: AnyObject) {
              queue.cancelAllOperations()
          }
      }
      

      Where

      /// Simple image network operation
      
      class ImageDownloadOperation: DataOperation {
          init(url: URL, session: URLSession = .shared, networkCompletionHandler: @escaping (Result<UIImage, Error>) -> Void) {
              super.init(url: url, session: session) { result in
                  switch result {
                  case .failure(let error):
                      networkCompletionHandler(.failure(error))
      
                  case .success(let data):
                      guard let image = UIImage(data: data) else {
                          networkCompletionHandler(.failure(DownloadError.notImage))
                          return
                      }
      
                      networkCompletionHandler(.success(image))
                  }
              }
          }
      }
      
      /// Simple network data operation
      ///
      /// This can be subclassed for image-specific operations, JSON-specific operations, etc.
      
      class DataOperation: AsynchronousOperation {
          var downloadTask: URLSessionTask?
      
          init(url: URL, session: URLSession = .shared, networkCompletionHandler: @escaping (Result<Data, Error>) -> Void) {
              super.init()
      
              downloadTask = session.dataTask(with: url) { data, response, error in
                  defer { self.complete() }
      
                  guard let data = data, let response = response as? HTTPURLResponse, error == nil else {
                      networkCompletionHandler(.failure(error!))
                      return
                  }
      
                  guard 200..<300 ~= response.statusCode else {
                      networkCompletionHandler(.failure(DownloadError.invalidStatusCode(response)))
                      return
                  }
      
                  networkCompletionHandler(.success(data))
              }
          }
      
          override func main() {
              downloadTask?.resume()
          }
      
          override func cancel() {
              super.cancel()
      
              downloadTask?.cancel()
          }
      }
      
      /// 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 stateLock = NSLock()
      
          private var _executing: Bool = false
          override private(set) public var isExecuting: Bool {
              get {
                  stateLock.withCriticalScope { _executing }
              }
              set {
                  willChangeValue(forKey: "isExecuting")
                  stateLock.withCriticalScope { _executing = newValue }
                  didChangeValue(forKey: "isExecuting")
              }
          }
      
          private var _finished: Bool = false
          override private(set) public var isFinished: Bool {
              get {
                  stateLock.withCriticalScope { _finished }
              }
              set {
                  willChangeValue(forKey: "isFinished")
                  stateLock.withCriticalScope { _finished = newValue }
                  didChangeValue(forKey: "isFinished")
              }
          }
      
          /// Complete the operation
          ///
          /// This will result in the appropriate KVN of isFinished and isExecuting
      
          public func complete() {
              if isExecuting {
                  isExecuting = false
              }
      
              if !isFinished {
                  isFinished = true
              }
          }
      
          override public func start() {
              if isCancelled {
                  isFinished = true
                  return
              }
      
              isExecuting = true
      
              main()
          }
      
          override public func main() {
              fatalError("subclasses must override `main`")
          }
      }
      
      extension NSLock {
      
          /// Perform closure within lock.
          ///
          /// An extension to `NSLock` to simplify executing critical code.
          ///
          /// - parameter block: The closure to be performed.
      
          func withCriticalScope<T>(block: () throws -> T) rethrows -> T {
              lock()
              defer { unlock() }
              return try block()
          }
      }