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
. Callingstart
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 OperationQueue
s 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