iOS Operation Queues

One way to perform tasks concurrently in iOS is with the Operation and OperationQueue classes built on top of Grand Central Dispatch (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.

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.

OperationQueue

An operation queue invokes its queued Operation objects based on their priority and readiness. After you add an operation to a queue, it remains in the queue until the operation finishes its task. You can’t directly remove an operation from a queue after you add it.

Operation queues retain operations until the operations finish, and queues themselves are retained until all operations are finished. Suspending an operation queue with operations that aren’t finished can result in a memory leak.

An operation queue is the Cocoa equivalent of a concurrent dispatch queue and is implemented by the OperationQueue class. Whereas dispatch queues always execute tasks in first-in, first-out order, operation queues take other factors into account when determining the execution order of tasks. Primary among these factors is whether a given task depends on the completion of other tasks. You configure dependencies when defining your tasks and can use them to create complex execution-order graphs for your tasks.

Operation

Because the Operation class is an abstract class, you do not use it directly but instead subclass or use one of the system-defined subclasses (NSInvocationOperation or BlockOperation) to perform the actual task.

Despite being abstract, the base implementation of Operation does include significant logic to coordinate the safe execution of your task. The presence of this built-in logic allows you to focus on the actual implementation of your task, rather than on the glue code needed to ensure it works correctly with other system objects.

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

  • main()

If you are creating a concurrent operation, you need to override the following methods and properties at a minimum:

  • start()
  • isAsynchronous
  • isExecuting
  • isFinished

Operation objects maintain state information internally:

  • isReady
  • isExecuting
  • isFinished
  • isCancelled

Once you add an operation to a queue, the operation is out of your hands. The queue takes over and handles the scheduling of that task.

You should always support cancellation semantics in any custom code you write. In particular, your main task code should periodically check the value of the isCancelled property. If the property reports the value true, your operation object should clean up and exit as quickly as possible.

KVO-Compliant Properties

Both Operation and OperationQueue classes are key-value coding (KVC) and key-value observing (KVO) compliant for several of their properties.

Because an operation may execute in any thread, KVO notifications associated with that operation may similarly occur in any thread.

Examples

import Foundation
import UIKit

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
  }
}

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
  }()
}

class ImageDownloader: Operation {
  let photoRecord: PhotoRecord

  init(_ photoRecord: PhotoRecord) {
    self.photoRecord = photoRecord
  }

  override func main() {
    if isCancelled {
      return
    }

    guard let imageData = try? Data(contentsOf: photoRecord.url) else { return }

    if isCancelled {
      return
    }

    if !imageData.isEmpty {
      photoRecord.image = UIImage(data:imageData)
      photoRecord.state = .downloaded
    } else {
      photoRecord.state = .failed
      photoRecord.image = UIImage(named: "Failed")
    }
  }
}

class ImageFiltration: Operation {
  let photoRecord: PhotoRecord

  init(_ photoRecord: PhotoRecord) {
    self.photoRecord = photoRecord
  }

  override func main () {
    if isCancelled {
      return
    }

    guard photoRecord.state == .downloaded else {
      return
    }

    if let image = photoRecord.image,
       let filteredImage = applySepiaFilter(image) {
      photoRecord.image = filteredImage
      photoRecord.state = .filtered
    }
  }

  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 self.isCancelled {
      return nil
    }

    guard
      let outputImage = filter.outputImage,
      let outImage = context.createCGImage(outputImage, from: outputImage.extent)
    else {
      return nil
    }

    return UIImage(cgImage: outImage)
  }
}