Grouping Common Operations Together

So far, we’ve seen how to create concurrent operations and how to chain them one after the other. Together, these are excellent tools, but they can lead to something that’s a bit of a pain.

Unfortunately, you’ll quickly find that you’ll be repeating yourself often. For example, think about downloading and parsing content. How many times are you going to repeat this sequence in your app?

Today, we’ll look into how we can group operations together in order to keep our code DRY and make life a little easier on ourselves.

Creating a Group Operation

In order to wrap these operations together, we’re going to use another NSOperation.

That’s right! It’s operations all the way down!

We’re going to create an NSOperation that wraps its own NSOperationQueue. To begin, the queue will be suspended. Then when we get to the main() method, we’ll unsuspend the queue and let it do its thing!

class GroupOperation: ConcurrentOperation {
	let internalQueue = OperationQueue()
	let finishingOperation = BlockOperation(block: {})

	override init() {
		internalQueue.isSuspended = true
		super.init()
	}
}

In order to complete the operation, we’ll use a ‘BlockOperation’ that calls the complete method. Whenever we add an operation to our ‘GroupOperation’, we’ll create a dependency between it and the finishing operation.

Only once all the operations we added are complete will the finishing operation execute and complete the group. Ingenious, isn’t it?

final override func main() {
	finishingOperation.completionBlock = { [unowned self] in
		self.complete()
	}

	internalQueue.addOperation(finishingOperation)

	internalQueue.isSuspended = false
}

public func add(operation: Operation) {
	finishingOperation.addDependency(operation)

	internalQueue.addOperation(operation)
}

As for cancelling our GroupOperation, we’re in luck! If we ever need to cancel it, we can simply cancel the operations in its queue.

override func cancel() {
	internalQueue.cancelAllOperations()
	super.cancel()
}

And there you have it! A functional GroupOperation that will allow us to wrap our smaller blocks together!

Creating an API Operation from Download + Parse

Now that we have our GroupOperation ready to go, how exactly do we use it? My preferred way is subclassing, and then creating the necessary operations inside the initializer.

Let’s go back to our Download + Parse operation we had before. How would we create an APIOperation that could wrap these two?

I think it would look something like this:

class APIOperation : GroupOperation {
	init(request: RequestTemplate?, params: [String : JSONInitable.Type]) {
		self.downloadOperation = DownloadOperation(with: request)
		let parseOperation = ParseOperation<T>(parsingParams: params)

		super.init([])

		let adapterOperation = BlockOperation {
			[unowned downloadOperation, unowned parseOperation] in

			switch downloadOperation.result {
			case .success(let data):
				parseOperation.data = data
			case .failure(let error):
				self.fail(with: error)
			default:
				self.fail(with: "")
			}
		}

		let finishingOperation = BlockOperation {
			[unowned parseOperation] in

			switch parseOperation.result {
			case .success(let parsedObject):
				self.complete(with: parsedObject)
			case .failure(let error):
				self.fail(with: error)
			default:
				self.fail(with: "")
			}
		}

		finishingOperation.addDependency(parseOperation)
		parseOperation.addDependency(adapterOperation)
		adapterOperation.addDependency(downloadOperation)

		for operation in [downloadOperation, adapterOperation, parseOperation, finishingOperation] {
			self.add(operation)
		}
	}
}

Alright! With this class in hand, we can now change our code from repeating this everywhere:

To this:

Doesn’t that feel better?

Not only were we able to abstract away all this downloading and parsing, but we’re also giving it more meaning. Now, instead of seeing disjointed operations in our code, we can see a single APIOperation and think to ourselves “Ah, that’s what it means!”

It also makes cancelling and failure much easier to deal with since we can handle those cases inside our APIOperation. The rest of the app only needs to know about how to handle the failure, and not what caused it.

Grouping parallel Operations together

I’d be remiss if I didn’t tell you about the other great advantage you get from grouping your operations.

GroupOperation is also an excellent way to run many operations in parallel and know when they’re all done.

Imagine we have a bunch of image URLs and we want to download them and create an array of UIImages. Using a group operation is an excellent way of downloading in parallel, in the background. The class would end up looking something like this:

class ImagesDownloadOperation: GroupOperation {
	var imagesArray: [UIImage] = []
	
	init(imageURLs: [URL]) {
		super.init()
		
		for url in imageURLS {
			let imageDownload = DownloadOperation(url: url)
			imageDownload.completion {
				imagesArray.append(UIImage(data: imageDownload.data))
			}
		}
	}
}

And that’s it! Our GroupOperation will automatically finish once all the operations it contains are finished as well.

So let’s say you have an art gallery, and an IncredibleGalleryViewController. You want to fetch your gallery object, parse it, the use its image URLs to populate your View Controller before pushing it on the navigation stack. Your code would now look like:

Gallery Operation

The power of not repeating yourself

In this article we saw:

  • How to group commonly used operations together.
  • How grouping operations abstracts away the details and gives them meaning
  • How to use group operations to wrap operations in series (like downloading and parsing) or in parallel (like fetching images)

Next time, in the final article of this series, we’ll look at how to use NSOperations with UIKit. If you’ve been looking for an easy way to run your login flow or your onboarding, this’ll be the article for you!