Cancel Dependent Operation in Operation.completionBlock

Hi


I have a couple of operations as follows:


class Operation1: Operation
{
    private var _executing: Bool = false
    private var _finished: Bool = false

    var hasError = false
  
    override internal(set) var isExecuting: Bool
    {
        get
        {
            return _executing
        }
        set
        {
            willChangeValue(forKey: "isExecuting")
            _executing = newValue
            didChangeValue(forKey: "isExecuting")
        }
    }

    override internal(set) var isFinished: Bool
    {
        get
        {
            return _finished
        }
        set
        {
            willChangeValue(forKey: "isFinished")
            _finished = newValue
            didChangeValue(forKey: "isFinished")
        }
    }

    /*
     Begins the execution of the operation.
     */
    override func main()
    {
        if isCancelled
        {
            isFinished = true
            return
        }
     
        isExecuting = true
     
        // Do something, but results in a failure
       self.hasError = true 
        self.finished()
    }

    /*
     Set the flag to signal a completion operation.
     */
    func finished()
    {
        self.isExecuting = false
        self.isFinished = true
    }
}

class Operation2: Operation
{
    private var _executing: Bool = false
    private var _finished: Bool = false

    var hasError = false
  
    override internal(set) var isExecuting: Bool
    {
        get
        {
            return _executing
        }
        set
        {
            willChangeValue(forKey: "isExecuting")
            _executing = newValue
            didChangeValue(forKey: "isExecuting")
        }
    }

    override internal(set) var isFinished: Bool
    {
        get
        {
            return _finished
        }
        set
        {
            willChangeValue(forKey: "isFinished")
            _finished = newValue
            didChangeValue(forKey: "isFinished")
        }
    }

    /*
     Begins the execution of the operation.
     */
    override func main()
    {
        if isCancelled
        {
            isFinished = true
            return
        }
     
        isExecuting = true
     
        // Do something
        self.finished()
    }

    /*
     Set the flag to signal a completion operation.
     */
    func finished()
    {
        self.isExecuting = false
        self.isFinished = true
    }
}


In my view controller, I construct Operation1 and Operation2 making Operation2 a dendency of Operation1. In the main function of Operation1, I set a property hasError (bool) if an error occurs. The operation1.completionBlock calls cancelAllOperations() is operation1.hasError equal true.


let queue = OperationQueue.main
           
let operation1 = Operation1()
operation1.completionBlock = {
  if operation1.hasError
  {
     print("Failed, cancelling pending operations.")
     queue.cancelAllOperations()
  }
}
           
let operation2 = Operation2()
operation2.completionBlock = {
  DispatchQueue.main.async
  {
    // Do something on UI
  }
}
               
operation2.addDependency(operation1)
queue.addOperation(operation1)
queue.addOperation(operation2)


However, Operation2 continues to execute. What is the best approach to cancel Operation2 if Operation1 fails?


Thanks

Accepted Reply

With multiple rows in the table, I'm noticing that the OperationQueue.main (for each row) is not executing asynchronously. If that because I'm using OperationQueue.main?

I presume you mean “because I’m scheduling the operations on the main queue?”, in which case the answer is yes. If you schedule an operation on the main queue then the

main()
function of that operation runs on the main thread, which isn’t helpful if you’re trying to get it to run in parallel.

To address this you’ll need to create your own operation queue and schedule your operations on that. In many cases you can use a global (or static) variable for this, because it’s fine for the queue to be long-lived.

Also, you can avoid the dispatch async to the main thread by creating a third operation.

let op1 = … as before …
let op2 = … as before …
let op3 = BlockOperation {
    … main thread stuff …
}

op2.addDependency(op1)
op3.addDependency(op2)

queue.addOperation(op1)
queue.addOperation(op2)
queue.addOperation(op3)

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Replies

What is the best approach to cancel Operation2 if Operation1 fails?

I usually just have Op2 look at the result of Op1 and fail if Op1 failed. This makes sense, at least the way I look at it: if Op2 is dependent on Op1 then Op2 has to actually use the results of Op1 somehow. If Op1’s result is missing, because it got cancelled or for any other reason, Op2 can’t proceed.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Hi eskimo


Thanks for the reply. Do you have an example of how you explained this. I thought the queue.cancelAllOperations() calls cancel() on operations in the queue, which signals the finished = true on any executing operations i.e operation2. Is there a reason why you can't use the completionBlock of operation1 to call cancelAllOperations()?


I'm calling constructing the queue and adding the operations for each cell:


func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell




Thanks again

Do you have an example of how you explained this.

Sure. But perhaps first you can explain why your operations are dependent. What about Op1 does Op2 depend on?

For example, one case I’ve used in the past in a JSON parser operation (Op2) that’s dependent on an HTTP GET operation (Op1). The JSON parser grabs the HTTP data from the HTTP GET operation, and thus it’s easy for it to determine if it was cancelled (or failed for some other reason).

ps These days I don’t have them so tightly coupled, but instead use the technique I outlined in this post.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

So I ended up handling the cancel in operation2 by overriding isCancelled


override internal(set) var isCancelled: Bool
{
  get
  {
    return _cancelled
  }
  set
  {
    willChangeValue(forKey: "isCancelled")
    _cancelled = newValue
    didChangeValue(forKey: "isCancelled")
  }
}

override var isAsynchronous: Bool
{
  return true
}

override func cancel()
{
  print("Operation cancelled.")
  _cancelled = true
}


So now the cellForRowAt function looks like this:


func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
  let cell = tableView.dequeueReusableCell(withIdentifier: "AccountCell", for: indexPath)
  let operation1 = Operation1()
  operation1.completionBlock = {
      DispatchQueue.main.async
      {
          if (operation1?.hasError)!
          {
              operation2.cancel()
          }
      }
 }


let operation2 = Operation2()      
operation2.completionBlock = {
  DispatchQueue.main.async
  {
    // Do something on UI
    cell.textLabel.text = operation2.name
  }
let queue = OperationQueue.main}

operation2.addDependency(operation1)
queue.addOperation(operation1)
queue.addOperation(operation2)

        return cell
}

This approach seems to work ok. With multiple rows in the table, I'm noticing that the OperationQueue.main (for each row) is not executing asynchronously. If that because I'm using OperationQueue.main? If so, how do I go about enabling the OperationQueue async?


Thanks again

With multiple rows in the table, I'm noticing that the OperationQueue.main (for each row) is not executing asynchronously. If that because I'm using OperationQueue.main?

I presume you mean “because I’m scheduling the operations on the main queue?”, in which case the answer is yes. If you schedule an operation on the main queue then the

main()
function of that operation runs on the main thread, which isn’t helpful if you’re trying to get it to run in parallel.

To address this you’ll need to create your own operation queue and schedule your operations on that. In many cases you can use a global (or static) variable for this, because it’s fine for the queue to be long-lived.

Also, you can avoid the dispatch async to the main thread by creating a third operation.

let op1 = … as before …
let op2 = … as before …
let op3 = BlockOperation {
    … main thread stuff …
}

op2.addDependency(op1)
op3.addDependency(op2)

queue.addOperation(op1)
queue.addOperation(op2)
queue.addOperation(op3)

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"