Swift Operation and OperationQueue

This is based on Operation and OperationQueue Tutorial in Swift article with some modifications.

Let’s start with this: your client wants an app which downloads images from the internet and applies some filters on them then shares them with other apps or saves in the camera roll. The easy way would be downloading them one by one then going back and applying the filter(s) one by one again, a bit painful. The other option would be using OperationQueue which makes things easier for you a lot.

This is what happens in this scenario: You create two queues, one for downloading images, the other for applying a filter on the images; In the next step you add operations to the queues respectively. When the first queue, download queue, finishes its job, it sends a notification to inform the system it’s done with downloading, then the second queue will be filled with new operations to apply the filter on the images and a another notification will be send when the queue is finished with filters.

Apple defines an Operation class as such:

An abstract class that represents the code and data associated with a single task.

Before we proceed, it should be noted that an Operation can be either synchronous or asynchronous. By default they are synchronous, but since our operation, downloading from an internet location, is async we need to subclass it and set the isAsynchronous true in addition to a little more modification.

class AsyncOperation: Operation { override var isAsynchronous: Bool { return true } private let _queue = DispatchQueue(label: "asyncOperationQueue", attributes: .concurrent) private var _isExecuting: Bool = false override var isExecuting: Bool { set { willChangeValue(forKey: "isExecuting") _queue.async(flags: .barrier) { self._isExecuting = newValue } didChangeValue(forKey: "isExecuting") } get { return _isExecuting } } var _isFinished: Bool = false override var isFinished: Bool { set { willChangeValue(forKey: "isFinished") _queue.async(flags: .barrier) { self._isFinished = newValue } didChangeValue(forKey: "isFinished") } get { return _isFinished } } }

We need to override isExecuting and isFinished properties so the OperationQueue will know when the operation is finished. Now AsyncOperation can be used as the parent class for our Operation subclasses. What we need now is an Operation subclass which can asynchronously download an image from a URL. Before continuing our journey to the operations, let’s have another class which holds needed data for our images and call it PhotoRecord. This class will need three properties, the URL of an image, the downloaded image and another property to keep track of its state:

enum OperationState { case new, downloading, downloaded, filtered, failed } class PhotoRecord { let url: URL var image: UIImage? = nil var state = OperationState.new init(url: URL) { self.url = url } }

Going back to operations, this subclass is responsible for downloading the image from a URL. For the download part a helper function is used, for the sake of simplicity. When you subclass an Operation, the main function will be called when it’s initialized. Notice how isExecuting and isFinished are used. In the main function first it checks if the operation is not canceled; If not, it tells the parent queue that the operation has started and when download, which is asynchronous, is finish it tells the queue it’s done.

class DownloadOperation: AsyncOperation { let photoRecord: PhotoRecord init(_ photoRecord: PhotoRecord) { self.photoRecord = photoRecord } override func main() { if isCancelled { return } isExecuting = true isFinished = false downloader(url: photoRecord.url) { (result) in switch result { case .failure: self.photoRecord.state = .failed case .success(let image): self.photoRecord.state = .downloaded self.photoRecord.image = image } self.isExecuting = false self.isFinished = true } } }

Similar to the DownloadOperation, we have another operation which applies a monochrome effect to the images:

class ImageFilterOperation: AsyncOperation { let photoRecord: PhotoRecord init(_ photoRecord: PhotoRecord) { self.photoRecord = photoRecord } override func main() { if isCancelled { return } isExecuting = true isFinished = false guard let currentCGImage = photoRecord.image?.cgImage else { self.photoRecord.state = .failed self.isExecuting = false self.isFinished = true return } let currentCIImage = CIImage(cgImage: currentCGImage) let filter = CIFilter(name: "CIColorMonochrome") filter?.setValue(currentCIImage, forKey: "inputImage") filter?.setValue(CIColor(red: 0.65, green: 0.65, blue: 0.65), forKey: "inputColor") filter?.setValue(1.0, forKey: "inputIntensity") guard let outputImage = filter?.outputImage else { return } let ciContext = CIContext() if let cgimg = ciContext.createCGImage(outputImage, from: outputImage.extent) { let processedImage = UIImage(cgImage: cgimg) self.photoRecord.image = processedImage self.photoRecord.state = .filtered self.isExecuting = false self.isFinished = true }else{ self.photoRecord.state = .failed self.isExecuting = false self.isFinished = true } } }

Now that we are done with operations, it’s time to add them to the queues. For each group of actions we need a separate queue and another property to keep track of operations, .done, .failed, etc. Let’s wrap these in a PendingOperations class.

class PendingOperations { lazy var downloadInProgress: [Int: Operation] = [:] lazy var downloadQueue: OperationQueue = { var queue = OperationQueue() queue.name = "Download Queue" return queue }() lazy var filteringInProgress: [Int: Operation] = [:] lazy var filterQueue: OperationQueue = { var queue = OperationQueue() queue.name = "Filter Queue" return queue }() }

“A queue that regulates the execution of operations.” Says Apple documents. OperationQueue inherits from NSObject, therefore it’s a KVO-complient class which helps us to know about the current state of the queue.

With this last morsel, we are done with the logic of the app. Now we have to put together the pieces.

The operation will start running when it’s added to the queue, so we need to create our DownloadOperation‘s then add them to the DownloadQueue. Now you can see how that state is useful here; When we initialize a PhotoRecord, its state value is .new, so we can track the state and run the appropriate operation .The whole code is not posted here to make it easier to read, but you can download it from github.

var photos = [PhotoRecord]() listOfImages.append(URL.init(string: "https://picsum.photos/id/1/500/500")!) listOfImages.append(URL.init(string: "https://picsum.photos/id/2/500/500")!) listOfImages.append(URL.init(string: "https://picsum.photos/id/3/500/500")!) for item in listOfImages { let photo = PhotoRecord(url: item) photos.append(photo) }

The next step is creating a DownloadOperation for each PhotoRecord and add them to the DownloadQueue and wait for the queue to finish. Afterwards, we should create ImageFilterQueue and add ImageFilterOperation objects to it and wait for it to finish its job. After this last step, we will have images which are fetched from URLs and modified with a monochrome filter, ready to be saved in the gallery, shared with other apps or just be show up on the screen.

func runQueues(){ for (index, item) in self.photos.enumerated() { startOperations(for: item, at: index) } } func startOperations(for photoRecord: PhotoRecord, at index: Int) { switch (photoRecord.state) { case .new: startRetrieving(for: photoRecord, at: index) case .downloaded: startApplyingFilter(for: photoRecord, at: index) default: break } } func startRetrieving(for photoRecord: PhotoRecord, at index: Int) { guard pendingOperations.downloadInProgress[index] == nil else { return } let download = DownloadOperation(photoRecord) download.completionBlock = { if download.isCancelled { return } DispatchQueue.main.async { self.pendingOperations.downloadInProgress.removeValue(forKey: index) } } pendingOperations.downloadInProgress[index] = download pendingOperations.downloadQueue.addOperation(download) } func startApplyingFilter(for photoRecord: PhotoRecord, at index: Int){ guard pendingOperations.filteringInProgress[index] == nil else { return } let filter = ImageFilterOperation(photoRecord) filter.completionBlock = { if filter.isCancelled { return } DispatchQueue.main.async { self.pendingOperations.filteringInProgress.removeValue(forKey: index) } } pendingOperations.filteringInProgress[index] = filter pendingOperations.filterQueue.addOperation(filter) }

We call runQueues() and the whole process begins as described. But there is a missing important thing: how can we know when a queue is finishes its job? The answer is observing the appropriate key path which is named "operations" and the sender object . To observe it, first we need to register an observer, in our case, two:

pendingOperations.downloadQueue.addObserver(self, forKeyPath: "operations", options: .new, context: nil) pendingOperations.filterQueue.addObserver(self, forKeyPath: "operations", options: .new, context: nil)

Then override addObserver(_:forKeyPath:options:context:) and listen for the right key path and object:

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { if object as? OperationQueue == pendingOperations.downloadQueue && keyPath == "operations" { if self.pendingOperations.downloadQueue.operations.isEmpty { pendingOperations.downloadQueue.removeObserver(self, forKeyPath: "operations") pendingOperations.filterQueue.addObserver(self, forKeyPath: "operations", options: .new, context: nil) self.runQueues() } }else if object as? OperationQueue == pendingOperations.filterQueue && keyPath == "operations" { if self.pendingOperations.filterQueue.operations.isEmpty { pendingOperations.filterQueue.removeObserver(self, forKeyPath: "operations") } } else { super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) } }

Here is the link to the complete project hosted on github.

Leave a comment

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.