Using Operations in your apps

Let's take a deep dive into Operations and refactor the background fetch code from the MustC app to make it use Operations. To do this, you will create two Operation subclasses: one that fetches data and updates a movie object, and one that calls the completion handler once all other operations are complete.

The setup will have a single OperationQueue that will execute all of the instances of the fetch Operation subclass and a single operation that calls the background fetch completion handler. The completion operation will have all of the fetch operations as its dependencies, so it's automatically executed when all fetch operations are completed.

Whenever you create an OperationQueue instance, you can specify the number of concurrent Operation instances that can be executed on the queue. If you set this number to one, you have a serial queue that runs all operations one by one in the order in which they become ready to execute.

An operation is considered ready when all preconditions for the operation are met. A great example of this is dependencies. An operation with dependencies is not ready to execute until all of the operations that it depends on are completed. Another example is exclusivity. It's possible to configure your setup to allow only a single operation of a particular type to be running at a time. An operation that is set up like that won't become ready until no other operations of that type are running.

If you set the maximum number of concurrent operations to a higher number, it's not guaranteed that the queue will run that number of operations simultaneously. Imagine setting the maximum amount to 1,000, and you put 2,000 operations in the queue. It's not likely that you will see 1,000 operations running in parallel. The system ultimately decides how many operations it will run at the same time, but it's never more than your maximum value.

As mentioned before, you can improve the MustC app by using OperationQueue. Get started with this refactor by creating an OperationQueue instance. You will add all download operations to this queue. Replace the implementation of application(_:performBackgroundFetchWithCompletionHandler:) in AppDelegate with the following:

func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {

  let queue = OperationQueue()

  let fetchRequest: NSFetchRequest<Movie> = Movie.fetchRequest()
  let managedObjectContext = persistentContainer.viewContext
  guard let allMovies = try? managedObjectContext.fetch(fetchRequest) else {
    completionHandler(.failed)
    return
  }
}

The preceding code creates an operation queue. This is the queue that will be used to fetch all movies and call the background fetch completion handler once all downloads are done. Before you can add operations to the queue, you must create the appropriate classes to contain your operation. Create a new file and name it UpdateMovieOperation.swift.

Every custom operation you create should subclass the Operation base class. The Operation class implements most of the glue and boilerplate code involved in managing and executing operations and queues. There are a couple of mandatory read-only variables that you need to implement for your operation. Also, you need to make sure these variables are KVO (Key-Value Observing) compliant, so the operation queue can observe the various states your operation will go through during its lifetime. Add the following basic implementation for UpdateMovieOperation:

import Foundation

class UpdateMovieOperation: Operation {
  override var isAsynchronous: Bool { return true }
  override var isExecuting: Bool { return _isExecuting }
  override var isFinished: Bool { return _isFinished }

  private var _isExecuting = false
  private var _isFinished = false
  var didLoadNewData = false

  let movie: Movie

  init(movie: Movie) {
    self.movie = movie
  }
}

You'll immediately notice that this implementation overrides a couple of variables. These are the read-only variables that were mentioned earlier. The isExecuting and isFinished variables return the value of two private variables that will be mutated to reflect the operation's state later.

Furthermore, the operation keeps track of whether new data was loaded, and there are a property and initializer to attach a Movie to the operation. So far, this operation isn't very exciting. Add the following methods to your operation so it can do something:

override func start() {
  super.start()

  willChangeValue(forKey: #keyPath(isExecuting))
  _isExecuting = true
  didChangeValue(forKey: #keyPath(isExecuting))

  let helper = MovieDBHelper()
  helper.fetchRating(forMovieId: movie.remoteId) { [weak self] id,
    popularity in
    defer {
      self?.finish()
    }

    guard let popularity = popularity,
      let movie = self?.movie,
      popularity != movie.popularity
      else { return }

    self?.didLoadNewData = true

    movie.managedObjectContext?.persist {
      movie.popularity = popularity
    }
  }
}

func finish() {
  willChangeValue(forKey: #keyPath(isFinished))
  _isFinished = true
  didChangeValue(forKey: #keyPath(isFinished))
}

Apple's guidelines state that every subclass of Operation should override the start() method and initiate the operation from there. You'll note that the superclass implementation is called first. This is because the superclass takes care of several under-the-hood tasks that must be performed to make the operation work. Next, willChangeValue(forKey:) and didChangeValue(forKey:) are called to fire the KVO notifications, so the operation queue knows when the state of the operation has changed or is about to change.

You might notice that the values of isFinished and isExecuted are not changed after calling willChangeValue(forKey:). This is okay because these methods only tell any observers that reading specific properties after didChangeValue(forKey:) will yield a different value than before. Since isFinished and isExecuting return the value of the private properties that are changed, isFinished and isExecuting will return different values, as the observer expects.

Next, the code from before is used to fetch and update the movie. A defer block is used to call the finish() method when the operation is done, regardless of how the network request went. By using defer instead of manually calling finish() when appropriate, you can't forget to call finish() if the code changes at some point. The finish() method makes sure that the operation queue is notified about the operation's new status by firing the corresponding KVO notifications.

Another operation that calls the background fetch completion handler should be created. This operation should loop through all of its dependencies, check whether it's a movie update operation, and if it is, it should check whether at least one of these operations has loaded new data. After doing this, the completion handler should be called with the corresponding result, and finally, the operation should finish itself. Create a new file in the Operations folder and name it BackgroundFetchCompletionOperation. Add the following implementation:

import Foundation
import UIKit

class BackgroundFetchCompletionOperation: Operation { override var isAsynchronous: Bool { return true } override var isExecuting: Bool { return _isExecuting } override var isFinished: Bool { return _isFinished } var _isExecuting = false var _isFinished = false let completionHandler: (UIBackgroundFetchResult) -> Void init(completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { self.completionHandler = completionHandler } override func start() { super.start() willChangeValue(forKey: #keyPath(isExecuting)) _isExecuting = true didChangeValue(forKey: #keyPath(isExecuting)) var didLoadNewData = false for operation in dependencies { guard let updateOperation = operation as? UpdateMovieOperation else { continue } if updateOperation.didLoadNewData { didLoadNewData = true break } } if didLoadNewData { completionHandler(.newData) } else { completionHandler(.noData) } willChangeValue(forKey: #keyPath(isFinished)) _isFinished = true didChangeValue(forKey: #keyPath(isFinished)) } }

The implementation for this operation is pretty similar to the movie update operation. The operation should be initialized with the completion handler that was passed to the background fetch method in AppDelegate, and it's called after figuring out whether new data was fetched by looping through all movie update operations. Let's see how all this comes together by updating the background fetch logic in AppDelegate. Add the following code to the application(_:performFetchWithCompletionHandler:) method, right after fetching the movies:

let completionOperation = BackgroundFetchCompletionOperation(completionHandler: completionHandler)

for movie in allMovies {
  let updateOperation = UpdateMovieOperation(movie: movie)
  completionOperation.addDependency(updateOperation)

  queue.addOperation(updateOperation)
}

queue.addOperation(completionOperation)

This code is a lot more readable than what was there before. First, the completion operation is created. Next, an update operation is created for each movie, this operation is added as a dependency for the completion operation, and it's added to the operation queue. Once all dependencies are set up, the completion operation itself is added to the queue as well. All of the movie update operations will automatically start executing simultaneously, and once they're all done, the completion operation becomes ready to execute. Once this happens, the completion operation will run and the background fetch completion handler will be called.

Even though Operations involve a bit more boilerplate code regarding managing execution state, you do end up with code that is a lot easier to read and understand. This chapter only covered setting up dependencies for operation, but if you study Apple's Advanced NSOperations video that was mentioned earlier, you'll find that you can do powerful, complex, and amazing things with operations.