Concurrency

Introduction to Operation and OperationQueues in iOS and Swift

Anurag Ajwani
5 min readNov 9, 2021

For this post I assume you are already familiar with Grand Central Dispatch and the basics of concurrency in iOS.

Photo by Dan Gold on Unsplash

Let’s say you’re running a salad restaurant. You’re at the till. A customer arrives and places an order. You pass on the salad making responsibility to the team in the kitchen. To make the salad:

  1. Add lettuce
  2. Add tomatoes
  3. Add red onion
  4. Add sweetcorn
  5. Add tuna with sunflower oil

The process of making the salad has to go in this order. It’s one of those fancy salad that are laid out in a specific way. Except for the first step every subsequent step relies on the completion of the previous step.

How does a salad maker looks like in code? Let’s look at an example:

let saladBowl = SaladBowl()LettucePrepper().prep(saladBowl, onCompletion: { saladBowl in
TomatoesPrepper().prep(saladBowl, onCompletion: { saladBowl in
RedOnionPrepper().prep(saladBowl, onCompletion: { saladBowl in
SweetcornPrepper().prep(saladBowl, onCompletion: { saladBowl in
TunaPrepper().prep(saladBowl, onCompletion: { saladBowl in
// deliver salad
})
})
})
})
})

Each instance (lettucePrepper, tomatoesPrepper, redOnionPrepper, sweetcornPrepper and tunaPrepper) represents one task. Once an order is placed anyone can pick up the first task. As soon as they complete the task the onComplete function is called with the results to delivered ready to passed on the next task to perform their bit.

Notice how tomatoesPrepper depends on lettucePrepper to complete their task before tomatoesPrepper can start. redOnionPrepper depends on tomatoesPrepper and so on. Also notice how the code gets nested to follow the order. What if you had 20 steps instead of 5? You’ll have to nest 20 times. This code can soon get ugly.

So is there a better way to execute code without nesting? Is there a better way to visualise the dependencies between each step?

Yes of course there is! The answer is in the title of this post 😉. In this post we’ll be exploring managing dependent operations using Operation and OperationQueues.

Managing dependencies with Operation and OperationQueues

In this section we’ll solve our salad making problem with Operation and OperationQueues through an example app. We’ll start with an existing app. Then we’ll create a class to represent our salad ingredient prepping as an Operation. Finally we’ll convert the salad maker code to use Operation and OperationQueues.

Here is a summary of the steps we are going to take:

  1. Download the starter project
  2. Create an ingredient prepping Operation
  3. Make SaladMaker use the ingredient prepping Operation and OperationQueues

1. Download the starter project

Let’s start by downloading the starter pack. Open Terminal app and execute the following commands:

cd $HOME
curl https://github.com/anuragajwani/intro-operation-and-queues/archive/refs/tags/starter.zip -o starter-operation-queues.zip -L -s
unzip -q starter-operation-queues.zip
cd intro-operation-and-queues-starter
open -a Xcode SaladMaker.xcodeproj

In this post we’ll mostly focusing on the SaladMaker class in SaladMaker.swift file.

2. Create an ingredient prepping Operation

In the intro example each Prepper performs one single task i.e. prepping lettuce. Operations encapsulate a single task to be performed. Let’s represent Prepping and ingredient as an Operation. Add the following code at the end of SwiftMaker.swift:

class IngredientPrepper: Operation {    private let saladBowl: SaladBowl
private let ingredient: Ingredient
init(saladBowl: SaladBowl, ingredient: Ingredient) {
self.saladBowl = saladBowl
self.ingredient = ingredient
}
override func main() {
sleep(1)
self.saladBowl.ingredients.append(self.ingredient)
}
}

In the code above we have created a single Prepper class which subclasses the Operation class. The Operation class is meant to be subclassed and not used directly. That is we can’t create an instance of the Operation class.

Because the Operation class is an abstract class, you do not use it directly but instead subclass

Apple Operation documentation

In the code we only need to override one function to make our operation work. The function to override is the main function.

For non-concurrent operations, you typically override only one method: main

Apple Operation documentation

Here we include our code to execute. Note we aren’t executing our code asynchronously in a separate thread like we were doing before. Don’t worry about this as we’ll cover how we will do that in the next step.

You can delete the other Preppers classes as we will no longer be using these.

3. Make SaladMaker use the ingredient prepping Operation and OperationQueues

In the previous section we created an Ingredient prepping operation. Next let’s change our messy SaladMaker make function implementation from using callbacks to using the IngredientPrepper operation and OperationQueues.

Replace the make function code to the following:

let saladBowl = SaladBowl()
let ingredients: [Ingredient] = [.lettuce, .tomatoes, .redOnion, .sweetcorn, .tuna] //1 List ingredients in order
var operations: [Operation] = []
for (index, ingredient) in ingredients.enumerated() {
let operation = IngredientPrepper(saladBowl: saladBowl, ingredient: ingredient) // 2 create operation for each ingredient
operation.completionBlock = {
onIngrdientPrepped(ingredient)
}
if index > 0 { // 3 add previous ingredient operation as dependency (except for the first one (lettuce)
let previousOperation = operations[index - 1]
operation.addDependency(previousOperation)
}
operations.append(operation)
}
let operationQueue = OperationQueue() // 4 create operation queue
operationQueue.maxConcurrentOperationCount = 1 // 7
operationQueue.addOperations(operations, waitUntilFinished: false) // 5
operationQueue.addBarrierBlock {
// 6 handling completion of all operations
completionHandler()
}

In the code above we start off by setting the list of ingredients to include in the salad. Then we loop through the list of ingredients in order and create an instance of IngredientPrepper operation for each ingredient. Next we make the previous ingredient preparation as a dependency unless its the first ingredient (lettuce in this case).

At this point all operations have been created and their dependent operation (previous ingredient prepping operation) stated. Next we create an OperationQueue instance and add all the operations to the queue. Finally we completion handler (addBarrierBlock) to handle when all operations are completed.

You might be asking where do we state the thread or queue in which our operations are executing. OperationQueues by default execute all operations in a separate thread.

Operation queues use the Dispatch framework to initiate the execution of their operations. As a result, queues always invoke operations on a separate thread

-Apple OperationQueue documentation

OperationQueue’s infact make use of DispatchQueues under the hood. Note in the comment number 7 we are setting to maxConcurrentOperationCount to 1. OperationQueue will manage the number of dispatch queue’s for you based on this configuration.

That’s it! Run the app and see it in action.

Summary

In this post we learnt:

  • how to create an operation
  • how to declare dependencies between operations
  • how to create operations queue
  • how to handle the completion of all operations within a queue

Final Thoughts

You can find the full source code in my Github repositories:

Operations and operations queue add easy extensibility to your asynchronous code. It makes it easier to read and thus manage. However there is a new solution included within Swift 5.5 that can make it even simpler and easier–that is async and await language features. I will be covering that in a post in the future.

For more on iOS development follow me on Twitter or Medium!

--

--

Anurag Ajwani

Senior iOS Engineer at Travelperk. 7+ years experience with iOS and Swift. Blogging my knowledge and experience.