Getting a Handle on Operations

There are lots of options out there for learning about and improving your use of Operation and OperationQueue. Let’s check them out.

NSOperation has been around for a surprisingly long time. It was introduced with Mac OS X 10.5, released nearly 12 years ago. This predates even libdispatch. The API has seen pretty wide-spread adoption, but unfortunately it has not gotten a lot of attention since. NSOperation and NSOperationQueue have received few new APIs since their initial release. And, with the release of Combine, I expect that trajectory won’t change.

But, adopting Combine isn’t straightforward. For most projects, the OS requirements probably make it a non-starter, at least for a few years. And, you may have quite a lot of NSOperation code already implemented. For many, NSOperation is a common tool in their workflow.

But, let’s face it - it’s pretty hard to use.

Problem 1: Asynchronous Operations

One of the first problems people run into with Operations is making them asynchronous. By default, Operation subclasses expect to be synchronously executed. And, it turns out to be non-trivial to create a subclass that supports asynchronous execution. For an API that was ostensibly created for the purposes of easy concurrency, this is really a shame.

Operation and OperationQueue, and their dependency system rely on KVO for correct state management. It’s a cumbersome approach, and gets tricky if you need to support cancellation or time out.

Problem 2: Always Subclassing

BlockOperation is a super-handy subclass. It lets you define an inline operation, using a closure to capture local state. This often a huge time-saver compared to setting up a whole subclass which a bunch of properties all for a tiny, one-off bit of work.

Unfortunately, BlockOperation does not support any form of asynchronous execution. This really limits its usefulness, especially since so many modern APIs are themselves asynchronous. No callbacks, no dispatch, and on top of all that, you cannot easily support cancellation.

Problem 3: Passing Data Around

By far, the biggest challenge you’ll run into with the Operation APIs is getting data into and out of them. An obvious use of an operation is to produce a value. But, Operations’ completionBlock property has no parameters. How are you supposed to get the value out? And, what if you want to use the output from one operation as the input to another?

The stock Operation classes just don’t support these kinds things nicely. You can make it work, but you’ll end up with optionals every where and error-prone patterns.

Help?

Thankfully, there are a bunch of great resources out there to help. If you are getting started, check out Frank Courville’s awesome series on building robust, reliable code with NSOperations. It is very accessible, and has some good suggestions on how to pass data around without custom subclasses.

If you are more interested in building some Swift-based Operation infrastructure for your projects, you should definitely read Antoine Van Der Lee’s phenomenal series on the subject. He goes into great depth on many areas, particularly around building asynchronous Operations and passing data around with strong typing.

Finally, you might also want to check out a WWDC video on the topic. Despite the title, the presentation is great for those that aren’t very familiar with Operation and OperationQueue.

Pre-Built Stuff

There are also options out there if you’d prefer to use a pre-built solution. That WWDC video inspired a very popular library called ProcedureKit. It is incredibly well documented and packed with features. Definitely worth a look - I know a few app developers that use it with great success.

We also maintain a library called OperationPlus that is specially made to smooth out some of the rougher areas of Operation and OperationQueue without deviating to much from the original API. It’s been in use in Chime for a while, and is great for addressing the commonly-faced problems.

A Peek at OperationPlus

To address problem 1, OperationPlus comes with a core class called BaseOperation. It provides a foundation for building both synchronous and asynchronous Operation subclasses. It features a built-in timeout support, as well as a number of important guardrails to prevent common errors. For example - did you know that you can set a dependency on an Operation after it has finished execution without any error?

Problem 2 is tackled by a number of subclasses extend the concept of BlockOperation. This is pretty straightforward, but here’s an example to give you an idea:

let op = AsyncBlockOperation { (completionBlock) in
    DispatchQueue.global().async {
        // do some async work here
        completionBlock()
    }
}

The solution to problem 3 comes from a suite of classes. These are all made for strongly-typed, chainable operations. They also automatically establish their interdependencies, and expose completionHandler properties that provide easy access to produced values. Here’s a peek:

class MyConsumerOperation: ConsumerOperation<Int> {
    override func main(with value: Int) {
        // make use of value here, or automatically
        // fail if it wasn't successfully produced
    }
}

// establish a chain with dependencies
let intOp: ProducerOperation<Int> = ...
  
let op = MyConsumerOperation(producerOp: intOp, timeout: 30.0)

// can also access the produced Int value inline via a closure
intOp.resultCompletionBlock = { (value)
    // use value here
}

As a bonus, OperationPlus also comes with a companion micro-library called OperationTestingPlus. It includes some goodies for writing easier unit tests for your Operation subclasses.

Wrapping Up

It’s true that reactive programming provides some compelling alternatives to the model that Operation supports. And, now that we have Combine, I expect those approaches to rapidly gain traction in the Apple developer community. But, even then, my guess is that Operation is bound to be in use for many projects for years to come. That makes it a great API to learn and get better at, even if it does have some challenging aspects.

I really hope that the excellent work by Frank and Antoine help you learn and understand what’s going on with Operations. Even if you’ve been at it for years, I’m certain you’ll learn something by reading their work. I definitely have. And, if OperationPlus proves helpful to you, that’s fantastic! We’d love to hear what you think you about it.

Wed, Dec 11, 2019 - Matt Massicotte

Previous: Language Server Protocol Client in Swift
Next: The Complex and Sad State of Exceptions