Operation and OperationQueue Tutorial in Swift

In this tutorial, you will create an app that uses concurrent operations to provide a responsive interface for users by using Operation and OperationQueue. By James Goodwill.

4.6 (78) · 3 Reviews

Download materials
Save for later
Share
You are currently viewing page 3 of 3 of this article. Click here to view the first page.

Fine Tuning

You’ve come a long way in this tutorial! Your little project is responsive and shows lots of improvement over the original version. However, there are still some small details that are left to take care of.

You may have noticed that as you scroll in the table view, those off-screen cells are still in the process of being downloaded and filtered. If you scroll quickly, the app will be busy downloading and filtering images from the cells further back in the list even though they aren’t visible. Ideally, the app should cancel filtering of off-screen cells and prioritize the cells that are currently displayed.

Didn’t you put cancellation provisions in your code? Yes, you did — now you should probably make use of them! :]

Open ListViewController.swift. Go to the implementation of tableView(_:cellForRowAtIndexPath:), and wrap the call to startOperationsForPhotoRecord in an if statement, as follows:

if !tableView.isDragging && !tableView.isDecelerating {
  startOperations(for: photoDetails, at: indexPath)
}

You tell the table view to start operations only if the table view is not scrolling. These are actually properties of UIScrollView and, because UITableView is a subclass of UIScrollView, table views automatically inherit these properties.

Next, add the implementation of the following UIScrollView delegate methods to the class:

override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
  //1
  suspendAllOperations()
}

override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
  // 2
  if !decelerate {
    loadImagesForOnscreenCells()
    resumeAllOperations()
  }
}

override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
  // 3
  loadImagesForOnscreenCells()
  resumeAllOperations()
}

A quick walk-through of the code above shows the following:

  1. As soon as the user starts scrolling, you will want to suspend all operations and take a look at what the user wants to see. You will implement suspendAllOperations in just a moment.
  2. If the value of decelerate is false, that means the user stopped dragging the table view. Therefore you want to resume suspended operations, cancel operations for off-screen cells, and start operations for on-screen cells. You will implement loadImagesForOnscreenCells and resumeAllOperations in a little while, as well.
  3. This delegate method tells you that table view stopped scrolling, so you will do the same as in #2.

Now, add the implementation of these missing methods to ListViewController.swift:

func suspendAllOperations() {
  pendingOperations.downloadQueue.isSuspended = true
  pendingOperations.filtrationQueue.isSuspended = true
}

func resumeAllOperations() {
  pendingOperations.downloadQueue.isSuspended = false
  pendingOperations.filtrationQueue.isSuspended = false
}

func loadImagesForOnscreenCells() {
  //1
  if let pathsArray = tableView.indexPathsForVisibleRows {
    //2
    var allPendingOperations = Set(pendingOperations.downloadsInProgress.keys)
    allPendingOperations.formUnion(pendingOperations.filtrationsInProgress.keys)
      
    //3
    var toBeCancelled = allPendingOperations
    let visiblePaths = Set(pathsArray)
    toBeCancelled.subtract(visiblePaths)
      
    //4
    var toBeStarted = visiblePaths
    toBeStarted.subtract(allPendingOperations)
      
    // 5
    for indexPath in toBeCancelled {
      if let pendingDownload = pendingOperations.downloadsInProgress[indexPath] {
        pendingDownload.cancel()
      }
      pendingOperations.downloadsInProgress.removeValue(forKey: indexPath)
      if let pendingFiltration = pendingOperations.filtrationsInProgress[indexPath] {
        pendingFiltration.cancel()
      }
      pendingOperations.filtrationsInProgress.removeValue(forKey: indexPath)
    }
      
    // 6
    for indexPath in toBeStarted {
      let recordToProcess = photos[indexPath.row]
      startOperations(for: recordToProcess, at: indexPath)
    }
  }
}

suspendAllOperations() and resumeAllOperations() have straightforward implementations. OperationQueues can be suspended, by setting the suspended property to true. This will suspend all operations in a queue — you can’t suspend operations individually.

loadImagesForOnscreenCells() is a little more complex. Here’s what’s going on:

  1. Start with an array containing index paths of all the currently visible rows in the table view.
  2. Construct a set of all pending operations by combining all the downloads in progress and all the filters in progress.
  3. Construct a set of all index paths with operations to be cancelled. Start with all operations, and then remove the index paths of the visible rows. This will leave the set of operations involving off-screen rows.
  4. Construct a set of index paths that need their operations started. Start with index paths all visible rows, and then remove the ones where operations are already pending.
  5. Loop through those to be cancelled, cancel them, and remove their reference from PendingOperations.
  6. Loop through those to be started, and call startOperations(for:at:) for each.

Build and run and you should have a more responsive and better resource-managed application! Give yourself a round of applause!

Classic photos, loading things one step at a time!

Classic photos, loading things one step at a time!

Notice that when you finish scrolling the table view, the images on the visible rows will start processing right away.

Where to Go From Here?

You can download the completed version of the project using the Download Materials button at the top or bottom of this tutorial.

You’ve learned how to use Operation and OperationQueue to move long-running computations off of the main thread while keeping your source code maintainable and easy to understand.

But beware — like deeply-nested blocks, gratuitous use of multi-threading can make a project incomprehensible to people who have to maintain your code. Threads can introduce subtle bugs that may never appear until your network is slow, or the code is run on a faster (or slower) device, or one with a different number of cores. Test very carefully and always use Instruments (or your own observations) to verify that introducing threads really has made an improvement.

A useful feature of operations that isn’t covered here is dependency. You can make an operation dependent on one or more other operations. This operation then won’t start until the operations on which it depends have all finished. For example:

// MyDownloadOperation is a subclass of Operation
let downloadOperation = MyDownloadOperation()
// MyFilterOperation  is a subclass of Operation
let filterOperation = MyFilterOperation()

filterOperation.addDependency(downloadOperation)

To remove dependencies:

filterOperation.removeDependency(downloadOperation)

Could the code in this project be simplified or improved by using dependencies? Put your new skills to use and try it. :] An important thing to note is that a dependent operation will still be started if the operations it depends on are cancelled, as well as if they finish naturally. You’ll need to bear that in mind.

If you have any comments or questions about this tutorial or Operations in general, please join the forum discussion below!

James Goodwill

Contributors

James Goodwill

Author

Felicity Johnson

Tech Editor

Michael Briscoe

Final Pass Editor

Richard Critz

Team Lead

Over 300 content creators. Join our team.