There’s a bunch of different ways to design this. The basic tools at your disposal include:
The biggest challenge, IMO, is that of coupling. You want to avoid coupling a consumer operation to its producer operations for two reasons:
To facilitate operation reuse. One goal of using NSOperation is to build up a library of reusable parts that you can apply in a variety of circumstances. If your operations are tightly coupled, you won’t hit this goal.
Let’s consider the example you outlined. Imagine that your ‘parse’ operation (the consumer operation) was tightly coupled to your ‘fetch’ operation (the producer operation). That would severely limit the usefulness of the parse operation. For example, if the parse operation started out in an old project that used an NSURLConnection-based fetch operation, it would be hard to move it to a new project that use an NSURLSession-based fetch operation. Or a CloudKit-based fetch operation. Or whatever.
To allow for testing. Again considering your example: if the parse operation is tightly coupled to the fetch operation, you can’t test it as an independent unit.
I also try to avoid coupling my operations to any operation infrastructure. That way I can move operations between projects without having to bring the infrastructure along for the ride.
One approach I looked at was to use NSOperation’s completion block facility to push results from the producer. Alas, it’s not as helpful as it could be. The issue is that the completion block runs when the operation is finished, so it runs asynchronously with respect to the starting of dependent operations. That means you can’t use the completion block to transfer data from one operation to its dependency.
After zenning on this for multiple years, I now lean towards using adapter block operations. Such operations can shuffle data from the producer operations to the consumer operation, adapting the data as appropriate for the context.
IMPORTANT The adapter block operation is dependent on the consumer operations. It won’t run until they’ve finished, so it can safely look at their results. Also, the producer operation is dependent on the adapter block operation, so it won’t start until the adapter block operation has finished its shuffling.
So, the code might look like this:
class FetchOperation: NSOperation {
var url: NSURL?
var data: NSData?
var error: NSError?
convenience init(url: NSURL) {
self.init()
self.url = url
}
// I've omitted the intricacies of implementing an async
// network operation because that’s too complex a topic to
// cover here. The only thing to watch out for is that, for
// adapter operations to work in a fully composible fashion,
// the start of the async code must check self.error and fail
// immediately if it’s set.
}
class ParseOperation: NSOperation {
var data: NSData?
var error: NSError?
convenience init(data: NSData) {
self.init()
self.data = data
}
override func main() {
if self.error == nil {
if let data = self.data {
self.parseData(data)
} else {
// We have no data to parse; this shouldn't happen.
fatalError()
}
}
}
func parseData(data: NSData) {
// ... parse the data ...
}
}
let fetchOp = FetchOperation(url: url)
let parseOp = ParseOperation()
let adapterOp = NSBlockOperation(block: {
parseOp.data = fetchOp.data
parseOp.error = fetchOp.error
})
adapterOp.addDependency(fetchOp)
parseOp.addDependency(adapterOp)
networkQueue.addOperation(fetchOp)
computeQueue.addOperation(adapterOp)
computeQueue.addOperation(parseOp)
Share and Enjoy
—
Quinn "The Eskimo!"
Apple Developer Relations, Developer Technical Support, Core OS/Hardware
let myEmail = "eskimo" + "1" + "@apple.com"