Operation

How often do you see someone use DispatchQueue? Now how often do you see Operation and OperationQueue? I feel like the Operation class is severely underused in most apps. Granted, DispatchQueue works fine, and Operation is just a wrapper for Grand Central Dispatch, but that doesn’t mean much in the Swift world. After all, Swift is a High Level Language. If we use that same argument, we could be writing in C++ or Assembly directly. So let’s take a look at just what Operation can do.

For starters, DispatchQueue doesn’t bode well for indentation. Once a few conditionals or loops appear the code starts looking more and more like a staircase:

func doIt() {
    DispatchQueue.global(qos: .background).async {
        if thisCondition {
            if thatCondition {
                // Processing...
            }
        }
    }
}

Unless you’re an Escher fan, I can’t imagine that looks great. So let’s start converting that async call into an Operation. Starting simply, we can use the pre-made BlockOperation All we need to do is call the initializer and pass in our code.

func doIt() {
    let operation = BlockOperation {
        if thisCondition {
            if thatCondition {
                // Processing...
            }
        }
    }
}

So… the code looks about the same, maybe a little bit more verbose than anything. And to top it off, this operation doesn’t even execute yet. In fact, even if we added operation.start() we wouldn’t be running asynchronously anyway. At this point, Operation is looking like more work than DispatchQueue.

So let’s start using Operation the way it was meant to be, by moving the block into an Operation subclass itself. This is fairly simple, override main() and add a few checks for isCancelled. Why check isCancelled? Because an Operation can be cancelled, unlike a DispatchQueue. This is great for long running tasks that might not even end up needed by the time they are finished.

class MyOperation: Operation {
    override func main() {
        guard !isCancelled else { return }

        if thisCondition {
            if thatCondition {
                // Processing...
            }
        }
    }
}

Note that we want to check isCancelled whenever we are going to take a bit of time to work. Loops are a great place to do this, and you might even think about using a good ‘ol for rather than forEach in those cases:

for object in objectList {
    guard !isCancelled else { return } 
    // Processing...
}

Well, it’s been a bit of extra overhead, but let’s move back to the call site. Now we’re finally taken a step out of our staircase.

func doIt() {
    let operation = MyOperation()
    if operation.isReady {
        operation.start()
    }
}

In the above code, we need to check if the operation’s isReady. Calling start before the operation is ready (or while it is already running) is counterintuitive and will cause a crash.

That looks much better. Of course, some operations will need data passed in. Since it is a subclass, we can add nice initializers and properties for setting the data. In fact, adding custom initializers will also make it easier to write tests, so win-win on that.

Once operation.start() returns, the operation is already complete, and the data is ready to use. This is helpful for breaking up code into multiple classes, but running on the same thread as our current code is not what DispatchQueue.async is doing (usually).

Fortunately, that is exactly why OperationQueues exist. In the most basic implementation, we just need to instantiate the queue and add our operation. Remember to keep the queue around though.

let operationQueue = OperationQueue()

func doIt() {
    let operation = MyOperation()
    operationQueue.addOperation(operation)
}

If we want to go even further, we can even set OperationQueue’s qualityOfService. This performs a role similar to .global(qos: .background) and completes the conversion from the DispatchQueue example at the start of this article.

let operationQueue: OperationQueue = {
    let queue = OperationQueue()
    queue.qualityOfService = .background
    return queue
}()

func doIt() {
    let operation = MyOperation()
    operationQueue.addOperation(operation)
}

At this point DispatchQueue has been replaced and we have split out our code into a separate, testable class. But what about callbacks and updating our UI? Fortunately, Operation has a completionBlock property that is automatically called by the queue! While this will introduce a block again, this block only cares about cleanup and after actions, and may not even be needed.

But wait there’s more! What if we have multiple operations to complete? Where that would require calling DispatchQueue.aysnc a couple of times, our OperationQueue already takes care of that. Adding another operation with addOperation(_:) is all that needs to be done. In fact, these operations can even be run concurrently or in order(!) by setting maxConcurrentOperationCount on the queue.

So I hope I’ve shown you some of the benefits of using Operations over straight DispatchQueue calls. We’re already using a high-level language, so how about we spend a little more time using high level constructs as well!


*If you have any questions, comments, or corrections let me know on Twitter