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