Understanding Operation and OperationQueue in Swift

There are more ways than one to do concurrent operations on iOS. In this article, we'll cover how to use Operations and OperationQueue to execute concurrent operations.

There are more ways than one to do concurrent operations on iOS. In this article, we’ll cover how to use Operations and OperationQueue to execute concurrent operations. There’s a lot to be said (and written) about concurrency on iOS and I don’t really want to bore you to death by writing a massive article about it, so I’ll focus here on Operations because chances are these will cover most of your needs for concurrent operations. We’ll touch a bit on GCDย because sometimes you just need to dig in deep and do it yourself.

Concurrency

This is just a fancy way of saying ‘doing stuff at the same time’. The iPhone is capable of executing multiple threads at the same time. I could go into the nitty gritty details about threads and queues, but you don’t really care about them. You just want to use your OperationQueue… Fair enough ๐Ÿ™‚

Just remember one thing – on your iOS device you’ll see two types of queues; the main queue, and everything else. In your day-to-day development, you can split all of your operations as being run on the main queue or off the main queue. Clearly, the main queue is important… So what is it exactly? Well, it’s where your UI gets updated. All the user interactions and all the animations get executed on the main queue. You don’t really want to run your code on the main queue because you’ll be blocking the UI and the user will see the UI as being unresponsiveย so they might even kill your app.

The Queues

All your UI updates should be performed on the main queue and you should do as much work as possible in the background, which is sometimes referred to as ‘off the main queue’. You want to keep the main queue clear.

The main queue is like a priority boarding queue at an airport. Some people buy priority boarding but most don’t and the people in the priority boarding queue board the plane before the people in the other queue (at least that’s how Ryanair does it ๐Ÿ™‚ ) Imagine what would happen if everyone bought a priority boarding pass. That queue would be full and the other queue would be empty so the priority queue would lose its purpose. The priority queue, just like the main queue, works only if you use it sparingly. There can be only one main queue and you can have as many background queues as you want.

The most common example would be refreshing a table view. You pull to refresh, start animating the spinner, kick off your networking request in the background and then when you’re finished processing and parsing the response you reload the table on the main queue.

Let’s use this example to demonstrate the usage of Operations and OperationQueue. I’ll use one of my projects on GitHub – DADependencyInjectionย – to demonstrate this.

Operation

Operation is an abstract class and represents a logical unit of work. For example, in your code you probably have a method that fetches data from the API. If you want to use OperationQueue you would have to create your class that subclasses Operation and move that method in your new class. Doing this has an added benefit of forcing you to use the single responsibility principle.

Once you have your class you add it to your OperationQueue and wait for it to finish executing. If you just want to execute a small piece of code or call a method you can use BlockOperation and NSInvocationOperation instead of subclassing Operation.

In our example we’ll use two operations, but first, we’ll create a base class for our operations just to get some of the clutter out of the way:

class DAOperation: Operation {
    
    private var _executing = false {
        willSet {
            willChangeValue(forKey: "isExecuting")
        }
        didSet {
            didChangeValue(forKey: "isExecuting")
        }
    }
    
    override var isExecuting: Bool {
        return _executing
    }
    
    private var _finished = false {
        willSet {
            willChangeValue(forKey: "isFinished")
        }
        
        didSet {
            didChangeValue(forKey: "isFinished")
        }
    }
    
    override var isFinished: Bool {
        return _finished
    }
    
    func executing(_ executing: Bool) {
        _executing = executing
    }
    
    func finish(_ finished: Bool) {
        _finished = finished
    }
}

We’ll create two operations: one for networking and one for parsing. Our networking operation will look like this:

class GetDataOperation: DAOperation {
    
    private let urlString: String
    private let provider: NetworkingProvider
    
    var responseData: Data?
    
    init(withURLString urlString: String, andNetworkingProvider provider: NetworkingProvider = AFNetworkConnector()) {
        self.urlString = urlString
        self.provider = provider
    }
    
    override func main() {
        guard isCancelled == false else {
            finish(true)
            return
        }
        
        executing(true)
        provider.restCall(urlString: urlString) { (data) in
            self.responseData = data
            self.executing(false)
            self.finish(true)
        }
    }
}

We’re initializing this class with a URL string and a networking provider. We’re exposing a ‘responseData’ property where we’ll save the networking response. This class is nothing more than a wrapper around our networking provider. Parse operation is even simpler:

Parse operation is even simpler:

class ParseDataOperation: DAOperation {
    
    private let factory: MoviesFactoryProvider
    
    var moviesData: Data?
    var movies: [MovieItem]?
    
    init(withFactory factory: MoviesFactoryProvider = MoviesFactory()) {
        self.factory = factory
    }
    
    override func main() {
        guard isCancelled == false else {
            finish(true)
            return
        }
        
        executing(true)
        guard
            let data = moviesData,
            let jsonObject = try? JSONSerialization.jsonObject(with: data, options: [JSONSerialization.ReadingOptions.allowFragments])
            else {
                executing(false)
                finish(true)
                return
        }
        
        movies = factory.movieItems(withJSON: jsonObject)
        
        executing(false)
        finish(true)
    }
}

We have two important properties at the top: ‘moviesData’ and ‘movies’. Networking operation will set the ‘moviesData’ property. We’ll feed this data into our factory and save the movies array into the instance variable ‘movies’.

When you start your operation the main method gets called. This method will be called by the OperationQueue automatically so all you have to do is add your operations to the queue.

OperationQueue

You create your OperationQueue and start adding your operations to it. OperationQueue is a prioritised FIFO queue which means you can set priority on individual operations. Those with the highest priority get pushed ahead, but not necessarily to the front of the queue, as iOS determines when to actually execute the operation. Operations with the same priority get executed in the order they were added to the queue unless an operation has dependencies.

You can set the priority of your operations by setting your queuePriority property of the operation. You can also control how much of the system resources will be given to your operation by setting qualityOfService property. OperationQueue can execute multiple operations at the same time. You can set the number of maxConcurrentOperationCount or you can let the system set this property to the default value which is different for each device.

A beautiful thing about operations is that they can be cancelled which is something you can’t do with blocks. Let’s say you have three operations: networking call, parsing of data and saving in the database. If your networking call fails, there’s little sense in executing the remaining two operations so you can simply cancel them.

In our example we’ll create a new data provider for movies. We’ll create a new property, ‘operationQueue’, and initialize it to the OperationQueue:

private let operationQueue: OperationQueue = OperationQueue()

Now all we need to do is create our operations and add them to the queue:

let networkingOperation = GetDataOperation(withURLString: urlString, andNetworkingProvider: networkingProvider)
let parsingOperation = ParseDataOperation(withFactory: moviesFactory)

operationQueue.addOperations([networkingOperation, parsingOperation], waitUntilFinished: false)

As soon as you add your operations to the queue they will start executing. As I said before the main method will get called for each operation when it starts executing.

Dependencies

In some cases your operations might depend on other operations to finish.

Your ParseDataOperation depends on the successful completion of the GetDataOperation. If your operation has dependencies it will not start until they all finish. This little feature is really powerful if you’re dealing with complex logic because it greatly simplifies your code.

Let’s go over this example for dependencies:

networkingOperation.completionBlock = {
   parsingOperation.moviesData = networkingOperation.responseData
}
        
parsingOperation.completionBlock = {
   onCompleted?(parsingOperation.movies ?? [])
}
parsingOperation.addDependency(networkingOperation)

We have our two operations again: parsingOperation and networkingOperation, but this time parsingOperation has a dependency set to networkingOperation. So parsingOperation will never start before networkingOperation.

Every operation has a ‘completionBlock’ that gets called when the operation finishes. We’ll use these completion blocks to pass data between operations. When the networking operation completes it sets the ‘moviesData’ property on the parsing operation. And when the parsing operation completes it calls the closure and passes in the movies.

At the end of the code listing, we’re setting our dependency.

What About GCD?

GCD stands for Grand Central Dispatch. You might think that with OperationQueues and Operations there’s no need for GCD but they actually work together. Fun fact: Operations are built on top of GCD.

I primarily use GCD to dispatch my closures back on the main thread. We talked a lot about Operations and OperationQueues here, but sooner or later you will have to update your UI to reflect the state of your app. UI updates must happen on the main thread so you need a way to dispatch code back to the main thread. One of the easiest ways to do that is to use GCD.

GCD is great if you need to use advanced things like semaphores and barriers. I won’t go into details on how to use them here but you can see an example of using barriers in my article on Singletons in Swift.

The most common use of GCD is to run a piece of code on the main thread – like the ‘onCompleted’ closure in our example here:

DispatchQueue.main.async(execute: { 
   onCompleted?(listItems)
})

Conclusion

OperationQueue is a great tool to use if you have complex logic in your app and if you have to manage dependencies between the operations. It’s very elegant at that. Passing data between operations is a bit messy. When your operation is finished you store the data in an instance variable which you read from the dependent operation. That’s not very elegant at all. There are much neater solutions out there like, for example, PromiseKit, which I’ll cover in one of the next articles.

I hope I gave you enough of an intro into Operations so you can use them in your apps where appropriate. You can find all the example code in my GitHub repo.

Have a nice day ๐Ÿ™‚

Dejan.

More resources

11 thoughts on “Understanding Operation and OperationQueue in Swift

  1. 18thcenturyprogrammer

    hello. thank you for great article.
    i am new to swift . could you tell me about this?

    parsingOperation.completionBlock = {
    onCompleted?(parsingOperation.movies ?? [])
    }

    what is the “onCompleted?” ?
    is this optional protocol method? and where is the onCompleted method from?

    Reply
  2. 18thcenturyprogrammer

    a ha~ caller side.i don’t know the term for this, but very interesting.
    i was reading your article to brush thing up. i just found your answer now. thanks

    Reply
  3. AT

    Hi,
    This is one of the best article on operation & operation queues.
    Can you explain in a bit more detail why did you create a new base class for operation, i can see you have used KVC but did not understand the purpose of it.
    Thank you

    Reply
    1. Dejan Agostini Post author

      Wow, thanks a lot AT, I appreciate it ๐Ÿ™‚

      You need to use KVO notifications to notify observers about the current state of the operation (as per Apple guidelines). If you don’t use KVO and don’t send the KVO notif for ‘finished’ property change, your operation would never get removed from the operation queue. That’s only one example. The short answer is, Apple is using KVO in the OperationQueues and they tell us to do it like this ๐Ÿ˜€

      Reply
  4. Pingback: ios์—์„œ์˜ thread ์ž‘์—…๊ด€๋ จ ์š”์•ฝ - Jacob is studying on programming

  5. Pingback: - Jacob is studying on programming

  6. Pingback: - Jacob is studying on programming

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.