Operation and OperationQueue Tutorial in Swift
In this tutorial, you will create an app that uses concurrent operations to provide a responsive interface for users by using Operation and OperationQueue. By James Goodwill.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Operation and OperationQueue Tutorial in Swift
35 mins
Operation vs. Grand Central Dispatch (GCD)
You may have heard of Grand Central Dispatch (GCD). In a nutshell, GCD consists of language features, runtime libraries, and system enhancements to provide systemic and comprehensive improvements to support concurrency on multi-core hardware in iOS and macOS. If you’d like to learn more about GCD, you can read our Grand Central Dispatch Tutorial.
Operation
and OperationQueue
are built on top of GCD. As a very general rule, Apple recommends using the highest-level abstraction, then dropping down to lower levels when measurements show this is necessary.
Here’s a quick comparison of the two that will help you decide when and where to use GCD or Operation
:
- GCD is a lightweight way to represent units of work that are going to be executed concurrently. You don’t schedule these units of work; the system takes care of scheduling for you. Adding dependency among blocks can be a headache. Canceling or suspending a block creates extra work for you as a developer!
- Operation adds a little extra overhead compared to GCD, but you can add dependency among various operations and re-use, cancel or suspend them.
This tutorial will use Operation
because you’re dealing with a table view and, for performance and power consumption reasons, you need the ability to cancel an operation for a specific image if the user has scrolled that image off the screen. Even if the operations are on a background thread, if there are dozens of them waiting on the queue, performance will still suffer.
Refined App Model
It is time to refine the preliminary non-threaded model! If you take a closer look at the preliminary model, you’ll see that there are three thread-bogging areas that can be improved. By separating these three areas and placing them in separate threads, the main thread will be relieved and can stay responsive to user interactions.
To get rid of your application bottlenecks, you’ll need a thread specifically to respond to user interactions, a thread dedicated to downloading data source and images, and a thread for performing image filtering. In the new model, the app starts on the main thread and loads an empty table view. At the same time, the app launches a second thread to download the data source.
Once the data source has been downloaded, you’ll tell the table view to reload itself. This has to be done on the main thread, since it involves the user interface. At this point, the table view knows how many rows it has, and it knows the URL of the images it needs to display, but it doesn’t have the actual images yet! If you immediately started to download all the images at this point, it would be terribly inefficient since you don’t need all the images at once!
What can be done to make this better?
A better model is just to start downloading the images whose respective rows are visible on the screen. So your code will first ask the table view which rows are visible and, only then, will it start the download tasks. Similarly, the image filtering tasks can’t begin until the image is completely downloaded. Therefore, the app shouldn’t start the image filtering tasks until there is an unfiltered image waiting to be processed.
To make the app appear more responsive, the code will display the image right away once it is downloaded. It will then kick off the image filtering, then update the UI to display the filtered image. The diagram below shows the schematic control flow for this:
To achieve these objectives, you’ll need to track whether the image is downloading, has downloaded, or is being filtered. You’ll also need to track the status and type of each operation, so that you can cancel, pause or resume each as the user scrolls.
Okay! Now you’re ready to get coding!
In Xcode, add a new Swift File to your project named PhotoOperations.swift. Add the following code:
import UIKit
// This enum contains all the possible states a photo record can be in
enum PhotoRecordState {
case new, downloaded, filtered, failed
}
class PhotoRecord {
let name: String
let url: URL
var state = PhotoRecordState.new
var image = UIImage(named: "Placeholder")
init(name:String, url:URL) {
self.name = name
self.url = url
}
}
This simple class represents each photo displayed in the app, together with its current state, which defaults to .new
. The image defaults to a placeholder.
To track the status of each operation, you’ll need a separate class. Add the following definition to the end of PhotoOperations.swift:
class PendingOperations {
lazy var downloadsInProgress: [IndexPath: Operation] = [:]
lazy var downloadQueue: OperationQueue = {
var queue = OperationQueue()
queue.name = "Download queue"
queue.maxConcurrentOperationCount = 1
return queue
}()
lazy var filtrationsInProgress: [IndexPath: Operation] = [:]
lazy var filtrationQueue: OperationQueue = {
var queue = OperationQueue()
queue.name = "Image Filtration queue"
queue.maxConcurrentOperationCount = 1
return queue
}()
}
This class contains two dictionaries to keep track of active and pending download and filter operations for each row in the table, and an operation queues for each type of operation.
All of the values are created lazily — they aren’t initialized until they’re first accessed. This improves the performance of your app.
Creating an OperationQueue
is very straightforward, as you can see. Naming your queues helps with debugging, since the names show up in Instruments or the debugger. The maxConcurrentOperationCount
is set to 1
for the sake of this tutorial to allow you to see operations finishing one by one. You could leave this part out and allow the queue to decide how many operations it can handle at once — this would further improve performance.
How does the queue decide how many operations it can run at once? That’s a good question! It depends on the hardware. By default, OperationQueue
does some calculation behind the scenes, decides what’s best for the particular platform it’s running on, and launches the maximum possible number of threads.
Consider the following example: Assume the system is idle and there are lots of resources available. In this case, the queue may launch eight simultaneous threads. Next time you run the program, the system may be busy with other, unrelated operations which are consuming resources. This time, the queue may launch only two simultaneous threads. Because you’ve set a maximum concurrent operations count in this app, only one operation will happen at a time.
Note: You might wonder why you have to keep track of all active and pending operations. The queue has an operations
method which returns an array of operations, so why not use that? In this project, it won’t be very efficient to do so. You need to track which operations are associated with which table view rows, which would involve iterating over the array each time you needed one. Storing them in a dictionary with the index path as a key means lookup is fast and efficient.
Note: You might wonder why you have to keep track of all active and pending operations. The queue has an operations
method which returns an array of operations, so why not use that? In this project, it won’t be very efficient to do so. You need to track which operations are associated with which table view rows, which would involve iterating over the array each time you needed one. Storing them in a dictionary with the index path as a key means lookup is fast and efficient.
It’s time to take care of download and filtration operations. Add the following code to the end of PhotoOperations.swift:
class ImageDownloader: Operation {
//1
let photoRecord: PhotoRecord
//2
init(_ photoRecord: PhotoRecord) {
self.photoRecord = photoRecord
}
//3
override func main() {
//4
if isCancelled {
return
}
//5
guard let imageData = try? Data(contentsOf: photoRecord.url) else { return }
//6
if isCancelled {
return
}
//7
if !imageData.isEmpty {
photoRecord.image = UIImage(data:imageData)
photoRecord.state = .downloaded
} else {
photoRecord.state = .failed
photoRecord.image = UIImage(named: "Failed")
}
}
}
Operation
is an abstract class, designed for subclassing. Each subclass represents a specific task as represented in the diagram earlier.
Here’s what’s happening at each of the numbered comments in the code above:
- Add a constant reference to the
PhotoRecord
object related to the operation. - Create a designated initializer allowing the photo record to be passed in.
-
main()
is the method you override inOperation
subclasses to actually perform work. - Check for cancellation before starting. Operations should regularly check if they have been cancelled before attempting long or intensive work.
- Download the image data.
- Check again for cancellation.
- If there is data, create an image object and add it to the record, and move the state along. If there is no data, mark the record as failed and set the appropriate image.
Next, you’ll create another operation to take care of image filtering. Add the following code to the end of PhotoOperations.swift:
class ImageFiltration: Operation {
let photoRecord: PhotoRecord
init(_ photoRecord: PhotoRecord) {
self.photoRecord = photoRecord
}
override func main () {
if isCancelled {
return
}
guard self.photoRecord.state == .downloaded else {
return
}
if let image = photoRecord.image,
let filteredImage = applySepiaFilter(image) {
photoRecord.image = filteredImage
photoRecord.state = .filtered
}
}
}
This looks very similar to the downloading operation, except that you’re applying a filter to the image (using an as yet unimplemented method, hence the compiler error) instead of downloading it.
Add the missing image filter method to the ImageFiltration
class:
func applySepiaFilter(_ image: UIImage) -> UIImage? {
guard let data = UIImagePNGRepresentation(image) else { return nil }
let inputImage = CIImage(data: data)
if isCancelled {
return nil
}
let context = CIContext(options: nil)
guard let filter = CIFilter(name: "CISepiaTone") else { return nil }
filter.setValue(inputImage, forKey: kCIInputImageKey)
filter.setValue(0.8, forKey: "inputIntensity")
if isCancelled {
return nil
}
guard
let outputImage = filter.outputImage,
let outImage = context.createCGImage(outputImage, from: outputImage.extent)
else {
return nil
}
return UIImage(cgImage: outImage)
}
The image filtering is the same implementation used previously in ListViewController
. It’s been moved here so that it can be done as a separate operation in the background. Again, you should check for cancellation very frequently; a good practice is to do it before and after any expensive method call. Once the filtering is done, you set the values of the photo record instance.
Great! Now you have all the tools and foundation you need in order to process operations as background tasks. It’s time to go back to the view controller and modify it to take advantage of all these new benefits.
Switch to ListViewController.swift and delete the lazy var photos
property declaration. Add the following declarations instead:
var photos: [PhotoRecord] = []
let pendingOperations = PendingOperations()
These properties hold an array of the PhotoRecord
objects and a PendingOperations
object to manage the operations.
Add a new method to the class to download the photos property list:
func fetchPhotoDetails() {
let request = URLRequest(url: dataSourceURL)
UIApplication.shared.isNetworkActivityIndicatorVisible = true
// 1
let task = URLSession(configuration: .default).dataTask(with: request) { data, response, error in
// 2
let alertController = UIAlertController(title: "Oops!",
message: "There was an error fetching photo details.",
preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default)
alertController.addAction(okAction)
if let data = data {
do {
// 3
let datasourceDictionary =
try PropertyListSerialization.propertyList(from: data,
options: [],
format: nil) as! [String: String]
// 4
for (name, value) in datasourceDictionary {
let url = URL(string: value)
if let url = url {
let photoRecord = PhotoRecord(name: name, url: url)
self.photos.append(photoRecord)
}
}
// 5
DispatchQueue.main.async {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
self.tableView.reloadData()
}
// 6
} catch {
DispatchQueue.main.async {
self.present(alertController, animated: true, completion: nil)
}
}
}
// 6
if error != nil {
DispatchQueue.main.async {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
self.present(alertController, animated: true, completion: nil)
}
}
}
// 7
task.resume()
}
Here’s what this does:
- Create a
URLSession
data task to download the property list of images on a background thread. - Configure a
UIAlertController
to use in the event of an error. - If the request succeeds, create a dictionary from the property list. The dictionary uses the image name as the key and its URL as the value.
- Build the array of
PhotoRecord
objects from the dictionary. - Return to the main thread to reload the table view and display the images.
- Display the alert controller in the event of an error. Remember that
URLSession
tasks run on background threads and display of any messages on the screen must be done from the main thread. - Run the download task.
Call the new method at the end of viewDidLoad()
:
fetchPhotoDetails()
Next, find tableView(_:cellForRowAtIndexPath:)
— it’ll be easy to find because the compiler is complaining about it — and replace it with the following implementation:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CellIdentifier", for: indexPath)
//1
if cell.accessoryView == nil {
let indicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
cell.accessoryView = indicator
}
let indicator = cell.accessoryView as! UIActivityIndicatorView
//2
let photoDetails = photos[indexPath.row]
//3
cell.textLabel?.text = photoDetails.name
cell.imageView?.image = photoDetails.image
//4
switch (photoDetails.state) {
case .filtered:
indicator.stopAnimating()
case .failed:
indicator.stopAnimating()
cell.textLabel?.text = "Failed to load"
case .new, .downloaded:
indicator.startAnimating()
startOperations(for: photoDetails, at: indexPath)
}
return cell
}
Here’s what this does:
- To provide feedback to the user, create a
UIActivityIndicatorView
and set it as the cell’s accessory view. - The data source contains instances of
PhotoRecord
. Fetch the correct one based on the current indexPath. - The cell’s text label is (nearly) always the same and the image is set appropriately on the
PhotoRecord
as it is processed, so you can set them both here, regardless of the state of the record. - Inspect the record. Set up the activity indicator and text as appropriate, and kick off the operations (not yet implemented).
Add the following method to the class to start the operations:
func startOperations(for photoRecord: PhotoRecord, at indexPath: IndexPath) {
switch (photoRecord.state) {
case .new:
startDownload(for: photoRecord, at: indexPath)
case .downloaded:
startFiltration(for: photoRecord, at: indexPath)
default:
NSLog("do nothing")
}
}
Here, you pass in an instance of PhotoRecord
along with its index path. Depending on the photo record’s state, you kick off either a download or filter operation.
Now you need to implement the methods that you called in the method above. Remember that you created a custom class, PendingOperations
, to keep track of operations; now you actually get to use it! Add the following methods to the class:
func startDownload(for photoRecord: PhotoRecord, at indexPath: IndexPath) {
//1
guard pendingOperations.downloadsInProgress[indexPath] == nil else {
return
}
//2
let downloader = ImageDownloader(photoRecord)
//3
downloader.completionBlock = {
if downloader.isCancelled {
return
}
DispatchQueue.main.async {
self.pendingOperations.downloadsInProgress.removeValue(forKey: indexPath)
self.tableView.reloadRows(at: [indexPath], with: .fade)
}
}
//4
pendingOperations.downloadsInProgress[indexPath] = downloader
//5
pendingOperations.downloadQueue.addOperation(downloader)
}
func startFiltration(for photoRecord: PhotoRecord, at indexPath: IndexPath) {
guard pendingOperations.filtrationsInProgress[indexPath] == nil else {
return
}
let filterer = ImageFiltration(photoRecord)
filterer.completionBlock = {
if filterer.isCancelled {
return
}
DispatchQueue.main.async {
self.pendingOperations.filtrationsInProgress.removeValue(forKey: indexPath)
self.tableView.reloadRows(at: [indexPath], with: .fade)
}
}
pendingOperations.filtrationsInProgress[indexPath] = filterer
pendingOperations.filtrationQueue.addOperation(filterer)
}
Here’s a quick list to make sure you understand what’s going on in the code above:
- First, check for the particular
indexPath
to see if there is already an operation indownloadsInProgress
for it. If so, ignore this request. - If not, create an instance of
ImageDownloader
by using the designated initializer. - Add a completion block which will be executed when the operation is completed. This is a great place to let the rest of your app know that an operation has finished. It’s important to note that the completion block is executed even if the operation is cancelled, so you must check this property before doing anything. You also have no guarantee of which thread the completion block is called on, so you need to use GCD to trigger a reload of the table view on the main thread.
- Add the operation to
downloadsInProgress
to help keep track of things. - Add the operation to the download queue. This is how you actually get these operations to start running — the queue takes care of the scheduling for you once you’ve added the operation.
The method to filter the image follows the same pattern, except it uses ImageFiltration
and filtrationsInProgress
to track the operations. As an exercise, you could try getting rid of the repetition in this section of code :]
You made it! Your project is complete. Build and run to see your improvements in action! As you scroll through the table view, the app no longer stalls and starts downloading images and filtering them as they become visible.
Isn’t that cool? You can see how a little effort can go a long way towards making your applications a lot more responsive — and a lot more fun for the user!