Operations & OperationQueues
Operations in Swift are a powerful way to separate responsibilities over several classes while keeping track of progress and dependencies. They're formally known as NSOperations and used in combination with the OperationQueue.
Operation
An Operation is typically responsible for a single synchronous task. It's an abstract class and never used directly.
Custom operation
You create separation of concern with custom operations. You could, for example, create a custom implementation for importing content and another one for uploading content
Different states of an operation
An operation can be in several states, depending on its current execution status.
Operation
An Operation is typically responsible for a single synchronous task. It's an abstract class and never used directly. You can make use of the system-defined BlockOperation subclass or by creating your own subclass. You can start an operation by adding it to an OperationQueue or by manually calling the start method. However, it's highly recommended to give full responsibility to the OperationQueue to manage the state.
let blockOperation = BlockOperation {
    print("Executing!")
}

let queue = OperationQueue()
queue.addOperation(blockOperation)

OR

queue.addOperation {
  print("Executing!")
}
Custom operation
You create separation of concern with custom operations. You could, for example, create a custom implementation for importing content and another one for uploading content.

The following code example shows a custom subclass for importing content:
final class ContentImportOperation: Operation {

    let itemProvider: NSItemProvider

    init(itemProvider: NSItemProvider) {
        self.itemProvider = itemProvider
        super.init()
    }

    override func main() {
        guard !isCancelled else { return }
        print("Importing content..")
        
        // .. import the content using the item provider

    }
}

let fileURL = URL(fileURLWithPath: "..")
let contentImportOperation = ContentImportOperation(itemProvider: NSItemProvider(contentsOf: fileURL)!)

contentImportOperation.completionBlock = {
    print("Importing completed!")
}

queue.addOperation(contentImportOperation)

// Prints:
// Importing content..
// Importing completed!
Different states of an operation
An operation can be in several states, depending on its current execution status.

  • Ready: It's prepared to start
  • Executing: The task is currently running
  • Finished: Once the process is completed
  • Canceled: The task canceled

It's important to know that an operation can only execute once. Whenever it's in the finished or canceled state, you can no longer restart the same instance.

Within custom implementations, you need to manually check the canceled state before execution to make sure a task cancels. Do know that a data race can occur when an operation is both started and canceled at the same time. You can read more about data races in blog post Thread Sanitizer explained: Data Races in Swift.

The OperationQueue will remove the task automatically from its queue once it becomes finished, which happens both after execution or cancellation.


Making use of dependencies

A benefit of using operations is the use of dependencies. You can easily add a dependency between two instances. For example, to start uploading after the content is imported:
let fileURL = URL(fileURLWithPath: "..")
let contentImportOperation = ContentImportOperation(itemProvider: NSItemProvider(contentsOf: fileURL)!)
contentImportOperation.completionBlock = {
    print("Importing completed!")
}

let contentUploadOperation = UploadContentOperation()
contentUploadOperation.addDependency(contentImportOperation)
contentUploadOperation.completionBlock = {
    print("Uploading completed!")
}

queue.addOperations([contentImportOperation, contentUploadOperation], waitUntilFinished: true)

// Prints:
// Importing content..
// Uploading content..
// Importing completed!
// Uploading completed!
The upload will only start after the content importation is finished. It does not take into account cancelation which means that if the import operation cancels, the upload would still start. You have to implement a check to see whether the dependencies were canceled or not
    final class UploadContentOperation: Operation {
    
        override func main() {
            guard !dependencies.contains(where: { $0.isCancelled }), !isCancelled else {
                return
            }
    
            print("Uploading content..")
        }
    }