In Part 1, we looked at the basics of the NSOperation
class and how to build our own. Today, we’ll taking things to the next level: we’ll look at how operations can work together in order to create new, complex behaviors.
Trust me: once you get familiar with this, you’ll see the uses everywhere.
This article is part of a 4-part series on Building robust, reliable code with NSOperations.
- Getting Started With NSOperation and NSOperationQueue
- Chaining a Series of NSOperations
- Grouping Common Operations Together
- Handling UI with NSOperations
Let’s have a look at the classic example of downloading and parsing data. We’ll start with 2 operations: one that downloads Data
from a URL, and another that parses Data
into a given model object. We want to achieve something like this:
As things stand, we have 2 challenges we need to overcome.
- First of all, we need to make sure that the
DownloadOperation
executes before theParseOperation
. This is obvious, but not quite straightforward. These operations are asynchronous, running on background threads. How can we guarantee that they’ll execute in the correct order? - Secondly, we need to pass the downloaded
Data
from ourDownloadOperation
to ourParseOperation
. Nothing exists to do this out of the box, so we’ll need to roll our own solution.
Let’s get started!
Dependencies — How to precisely control when your operations will execute
In the last article, we glossed over the Ready
state of the NSOperation
, but now it’s time to go more in depth. When an operation is Ready
, it means that all of its dependencies have completed.
But wait a sec Frank, what do you mean by dependencies?
If operation B depends on operation A, the system guarantees that operation B will never execute before operation A. Regardless of the fact that operations are asynchronous, we can still reason about their timing and execution.
Dependencies also work between different operation queues and threads. Thanks Apple! 🙌🏻
What’s more, creating a dependency between two operations is extremely easy. It’s as simple as this:
So this is how we get over our first hurdle: if we make our ParseOperation
depend on our DownloadOperation
, we no longer longer need to worry about them executing out of order. Great!
Adapters - How to pass data from one operation to another
At the moment we have 2 separate operations. I rather not combine them, since they each make sense on their own. A DownloadOperation
could download data that isn’t parsed (like a video), and a ParseOperation
could be used to parse data that isn’t downloaded (like a file on disk).
But now we need to get these operations to talk to each other. There are a few ways we could go about this, but my favourite is using a third operation: the adapter operation.
An adapter operation is simply a BlockOperation
that takes data from one operation and passes it to the other. These are single-use operations that we can build on the fly when we need them.
It’ll be sandwiched between our 2 operations. That means we need to change how our dependencies are handled. Instead of having our ParseOperation
depend on our DownloadOperation
, we’ll have it depend on our adapter operation. Also, our adapter operation will depend on our DownloadOperation
. It’ll look something like this:
This effectively creates a chain of operations that you know will execute in the right order. Here’s what the code would look like:
So what’s going on here exactly? When our DownloadOperation
finishes, the adapter operation will have all of its dependencies satisfied. Then it’ll execute, taking the data from the DownloadOperation
and setting it on the ParseOperation
. Once that’s done, the adapter operation will finish and the ParseOperation
will have its dependencies satisfied. It’ll execute and parse the data that was passed to it.
Pretty cool, n’est-ce pas?
Watch out for retain cycles and deadlocks
There’s one last thing we need to add to our example above, because as things stand, we have a nasty memory leak going on. To avoid this, we need to add a capture list to our adapter operation to weakly capture the references to the other operations. If we don’t, none of our operations will ever deallocate.
I’d like to extend my thanks to Nick Harris for figuring this out! You can find his article about it here: Retain Cycle with NSOperation Dependencies (His blog has plenty of amazing resources on NSOperations in Swift. I highly recommend poking around and reading it!)
Also, a quick word of warning. If you make 2 operations dependant on each other (i.e. operation A depends on B, and B depends on A), neither of them will execute. This is a deadlock, where each operation is waiting for the other to finish.
Feel the power!
There you have it! You’re now able to create Operations
and chain them together.
Though as you can imagine, you’ll often be downloading and parsing together. Next time, we’ll look at how we can wrap commonly used operations together so we can keep our code nice and DRY.