NSOperation and NSOperationQueue Tutorial in Swift

Richard Turton
Learn how to use NSOperations in your app!

Learn how to use NSOperations in your app!

Update 17 April 2015: Updated for Xcode 6.3 and Swift 1.2

Update note: This tutorial was updated to iOS 8, Xcode 6.1 and Swift by Richard Turton. Original post by Tutorial Team member Soheil Azarpour.

Everyone has had the frustrating experience of tapping a button or entering some text in an iOS or Mac app, when all of a sudden – WHAM, the user interface stops being responsive.

On the Mac, your users get to stare at the hourglass or the colorful wheel rotating for a while until they can interact with the UI again. In an iOS app, users expect apps to respond immediately to their touches. Unresponsive apps feel clunky and slow, and usually receive bad reviews.

Keeping your app responsive is easier said than done. Once your app needs to perform more than a handful of tasks, things get complicated quickly. There isn’t much time to perform heavy work in the main run loop and still provide a responsive UI.

What’s a poor developer to do? The solution is to move work off the main thread via concurrency. Concurrency means that your application executes multiple streams (or threads) of operations all at the same time – this way the user interface can stay responsive as you’re performing your work.

One way to perform operations concurrently in iOS is with the NSOperation and NSOperationQueue classes. In this tutorial, you’ll learn how to use them! You’ll start with an app that doesn’t use concurrency at all, so it will appear very sluggish and unresponsive. Then you will rework the application to add concurrent operations and — hopefully — provide a more responsive interface to the user!

Getting Started

The overall goal of the sample project for this tutorial is to show a table view of filtered images. The image will be downloaded from the Internet, have a filter applied, and then be displayed in the table view.

Here’s a schematic view of the app model:

Preliminary Model

Preliminary Model

A First Try

Download the first version of the project that you’ll be working on in this tutorial.

Note: All images are from stock.xchng. Some images in the data source are intentionally mis-named, so that there are instances where an image fails to download to exercise the failure case.

Build and run the project, and (eventually) you’ll see the app running with a list of photos. Try scrolling the list. Painful, isn’t it?

Classic photos, running slowly

Classic photos, running slowly

All of the action is taking place in ListViewController.swift, and most of that is inside tableView(_:cellForRowAtIndexPath:).

Have a look at that method and note there are two things taking place that are quite intensive:

  1. Loading the image data from the web. Even if this is easy work, the app still has to wait for the download to be complete before it can continue.
  2. Filtering the image using Core Image. This method applies a sepia filter to the image. If you would like to know more about Core Image filters, check out Beginning Core Image in Swift

In addition, you’re also loading the list of photos from the web when it is first requested:

  lazy var photos = NSDictionary(contentsOfURL:dataSourceURL)

All of this work is taking place on the main thread of the application. Since the main thread is also responsible for user interaction, keeping it busy with loading things from the web and filtering images is killing the responsiveness of the app. You can get a quick overview of this by using Xcode’s gauges view. You can get to the gauges view by showing the Debug navigator (Command-6) and then selecting CPU while the app is running.

Xcode's Gauges view, showing heavy lifting on the main thread

Xcode’s Gauges view, showing heavy lifting on the main thread

You can see all those spikes in Thread 1, which is the main thread of the app. For more detailed information you can run the app in Instruments, but that’s a whole other tutorial :].

It’s time to think about how can you improve that user experience!

Tasks, Threads and Processes

Before you dive into the tutorial, there are a few technical concepts that need to be dealt with. I’m going to define a few terms:

  • Task: a simple, single piece of work that needs to be done.
  • Thread: a mechanism provided by the operating system that allows multiple sets of instructions to operate at the same time within a single application.
  • Process: an executable chunk of code, which can be made up of multiple threads.

Note: In iOS and OS X, the threading functionality is provided by the POSIX Threads API (or pthreads), and is part of the operating system. This is pretty low level stuff, and you will find that it is easy to make mistakes; perhaps the worst thing about threads is those mistakes can be incredibly hard to find!

The Foundation framework contains a class called NSThread, which is much easier to deal with, but managing multiple threads with NSThread is still a headache. NSOperation and NSOperationQueue are higher level classes that have greatly simplified the process of dealing with multiple threads.

In this diagram, you can see the relationship between a process, threads, and tasks:

Process, Thread and Task

Process, Thread and Task

As you can see, a process can contain multiple threads of execution, and each thread can perform multiple tasks one at a time.

In this diagram, thread 2 performs the work of reading a file, while thread 1 performs user-interface related code. This is quite similar to how you should structure your code in iOS – the main thread should perform any work related to the user interface, and secondary threads should perform slow or long-running operations such as reading files, acccessing the network, etc.

NSOperation vs. Grand Central Dispatch (GCD)

You may have heard of Grand Central Dispatch (GCD). In a nutshell, GCD consists of language features, runtime libraries, and system enhancements to provide systemic and comprehensive improvements to support concurrency on multi-core hardware in iOS and OS X. If you’d like to learn more about GCD, you can read our Multithreading and Grand Central Dispatch on iOS for Beginners Tutorial.

NSOperation and NSOperationQueue are built on top of GCD. As a very general rule, Apple recommends using the highest-level abstraction, and then dropping down to lower levels when measurements show they are needed.

Here’s a quick comparison of the two that will help you decide when and where to use GCD or NSOperation:

  • 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! :]
  • NSOperation adds a little extra overhead compared to GCD, but you can add dependency among various operations and re-use, cancel or suspend them.

This tutorial will use NSOperation because you’re dealing with a table view and for performance and power consumption reasons you need to be able to cancel an operation for a specific image if the user has scrolled that image off the screen. Even if the operations are on a background thread, if there are dozens of them waiting on the queue then performance will still suffer.

Redefined App Model

It is time to redefine the preliminary non-threaded model! If you take a closer look at the preliminary model, you see that there are three thread-bogging areas that can be improved. By separating these three areas and placing them in a separate thread, the main thread will be relieved and it can stay responsive to user interactions.

Improved model

Improved model

To get rid of your application bottlenecks, you’ll need a thread specifically to respond to user interactions, a thread dedicated to downloading data source and images, and a thread for performing image filtering. In the new model, the app starts on the main thread and loads an empty table view. At the same time, the app launches a second thread to download the data source.

Once the data source has been downloaded, you’ll tell the table view to reload itself. This has to be done on the main thread, since it involves the user interface. At this point, the table view knows how many rows it has, and it knows the URL of the images it needs to display, but it doesn’t have the actual images yet! If you immediately started to download all images at this point, it would be terribly inefficient, as you don’t need all the images at once!

What can be done to make this better?

A better model is just to start downloading the images whose respective rows are visible on the screen. So your code will first ask the table view which rows are visible, and only then will it start the download process. As well, the image filtering process can’t be started before the image is completely downloaded. Therefore, the code should not start the image filtering process until there is an unfiltered image waiting to be processed.

To make the app appear more responsive, the code will display the image right away once it is downloaded. It will then kick off the image filtering, and then update the UI to display the filtered image. The diagram below shows the schematic control flow for this process:

NSOperation Control Flow

Control Flow

To achieve these objectives, you will need to track whether the image is currently being downloaded, is finished being downloaded, or if the image filtering has been applied. You will also need to track the status of each operation, and whether it is a downloading or filtering operation, so that you can cancel, pause or resume each as the user scrolls.

Okay! Now you’re ready to get coding! :]

Open the project where you left it off, and add a new Swift File to your project named PhotoOperations.swift. Add the following code:

import UIKit
 
// This enum contains all the possible states a photo record can be in
enum PhotoRecordState {
  case New, Downloaded, Filtered, Failed
}
 
class PhotoRecord {
  let name:String
  let url:NSURL
  var state = PhotoRecordState.New
  var image = UIImage(named: "Placeholder")
 
  init(name:String, url:NSURL) {
    self.name = name
    self.url = url
  }
}
Note: Make sure to import UIKit in the top of the file. By default, Xcode will only import Foundation to Swift files.

This simple class will represent each photo displayed in the app, together with its current state, which defaults to .New for newly created records. The image defaults to a placeholder.

To track the status of each operation, you’ll need a separate class. Add the following definition to the end of PhotoOperations.swift:

class PendingOperations {
  lazy var downloadsInProgress = [NSIndexPath:NSOperation]()
  lazy var downloadQueue:NSOperationQueue = {
    var queue = NSOperationQueue()
    queue.name = "Download queue"
    queue.maxConcurrentOperationCount = 1
    return queue
    }()
 
  lazy var filtrationsInProgress = [NSIndexPath:NSOperation]()
  lazy var filtrationQueue:NSOperationQueue = {
    var queue = NSOperationQueue()
    queue.name = "Image Filtration queue"
    queue.maxConcurrentOperationCount = 1
    return queue
    }()
}

This class contains two dictionaries to keep track of active and pending download and filter operations for each row in the table, and two operation queues for each type of operation.

All of the values are created lazily, meaning they aren’t initialized until they are first accessed. This improves the performance of your app.

Creating an NSOperationQueue is very straightforward, as you can see. Naming your queues helps, since the names show up in Instruments or the debugger. The maxConcurrentOperationCount is set to 1 here for the sake of this tutorial, to allow you to see operations finishing one by one. You could leave this part out to allow the queue to decide how many operations it can handle at once – this would further improve performance.

How does the queue decide how many operations it can run at once? That’s a good question! :] It depends on the hardware. By default, NSOperationQueue will do some calculation behind the scenes, decide what is best for the particular platform the code is running on, and will launch the maximum possible number of threads.

Consider the following example. Assume the system is idle, and there are lots of resources available, so the queue could launch something like eight simultaneous threads. Next time you run the program, the system could be busy with other unrelated operations which are consuming resources, and the queue will only launch two simultaneous threads. Because you’ve set a maximum concurrent operations count, in this app only one operation will happen at a time.

Note:You might wonder why you have to keep track of all active and pending operations. The queue has an operations method which returns an array of operations, so why not use that? In this project it won’t be very efficient to do so. You need to track which operations are associated with which table view rows, which would involve iterating over the array each time you needed one. Storing them in a dictionary with the index path as a key means lookup is fast and efficient.

It’s time to take care of download and filtration operations. Add the following code to the end of PhotoOperations.swift:

class ImageDownloader: NSOperation {
  //1
  let photoRecord: PhotoRecord
 
  //2
  init(photoRecord: PhotoRecord) {
    self.photoRecord = photoRecord
  }
 
  //3
  override func main() {
    //4
    if self.cancelled {
      return
    }
    //5
    let imageData = NSData(contentsOfURL:self.photoRecord.url)
 
    //6
    if self.cancelled {
      return
    }
 
    //7
    if imageData?.length > 0 {
      self.photoRecord.image = UIImage(data:imageData!)
      self.photoRecord.state = .Downloaded
    }
    else
    {
      self.photoRecord.state = .Failed
      self.photoRecord.image = UIImage(named: "Failed")
    }
  }
}

NSOperation is an abstract class, designed for subclassing. Each subclass represents a specific task as represented in the diagram earlier.

Here’s what’s happening at each of the numbered comments in the code above:

  1. Add a constant reference to the PhotoRecord object related to the operation.
  2. Create a designated initializer allowing the photo record to be passed in.
  3. main is the method you override in NSOperation subclasses to actually perform work.
  4. Check for cancellation before starting. Operations should regularly check if they have been cancelled before attempting long or intensive work.
  5. Download the image data.
  6. Check again for cancellation.
  7. If there is data, create an image object and add it to the record, and move the state along. If there is no data, mark the record as failed and set the appropriate image.

Next, you’ll create another operation to take care of image filtering! Add the following code to the end of PhotoOperations.swift:

class ImageFiltration: NSOperation {
  let photoRecord: PhotoRecord
 
  init(photoRecord: PhotoRecord) {
    self.photoRecord = photoRecord
  }
 
  override func main () {
    if self.cancelled {
      return
    }
 
    if self.photoRecord.state != .Downloaded {
      return
    }
 
    if let filteredImage = self.applySepiaFilter(self.photoRecord.image!) {
      self.photoRecord.image = filteredImage
      self.photoRecord.state = .Filtered
    }
  }
}

This looks very similar to the downloading operation, except that you’re applying a filter to the image (using an as yet unimplemented method, hence the compiler error) instead of downloading it.

Add the missing image filter method to the ImageFiltration class:

func applySepiaFilter(image:UIImage) -> UIImage? {
  let inputImage = CIImage(data:UIImagePNGRepresentation(image))
 
  if self.cancelled {
    return nil
  }
  let context = CIContext(options:nil)
  let filter = CIFilter(name:"CISepiaTone")
  filter.setValue(inputImage, forKey: kCIInputImageKey)
  filter.setValue(0.8, forKey: "inputIntensity")
  let outputImage = filter.outputImage
 
  if self.cancelled {
    return nil
  }
 
  let outImage = context.createCGImage(outputImage, fromRect: outputImage.extent())
  let returnImage = UIImage(CGImage: outImage)
  return returnImage
}

The image filtering is the same implementation used previously in ListViewController. It’s been moved here so that it can be done as a separate operation in the background. Again, you should check for cancellation very frequently; a good practice is to do it before and after any expensive method call. Once the filtering is done, you set the values of the photo record instance.

Great! Now you have all the tools and foundation you need in order to process operations as a background tasks. It’s time to go back to the view controller and modify it to take advantage of all these new benefits.

Switch to ListViewController.swift and delete the lazy var photos property declaration. Add the following declarations instead:

var photos = [PhotoRecord]()
let pendingOperations = PendingOperations()

These will hold an array of the PhotoDetails objects you created earlier, and the PendingOperations object to manage the operations.

Add a new method to the class to download the photos property list:

func fetchPhotoDetails() {
  let request = NSURLRequest(URL:dataSourceURL!)
  UIApplication.sharedApplication().networkActivityIndicatorVisible = true
 
  NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue()) {response,data,error in
    if data != nil {
      let datasourceDictionary = NSPropertyListSerialization.propertyListWithData(data, options: Int(NSPropertyListMutabilityOptions.Immutable.rawValue), format: nil, error: nil) as! NSDictionary
 
      for(key : AnyObject,value : AnyObject) in datasourceDictionary {
        let name = key as? String
        let url = NSURL(string:value as? String ?? "")
        if name != nil && url != nil {
          let photoRecord = PhotoRecord(name:name!, url:url!)
          self.photos.append(photoRecord)
        }
      }
 
      self.tableView.reloadData()
    }
 
    if error != nil {
      let alert = UIAlertView(title:"Oops!",message:error.localizedDescription, delegate:nil, cancelButtonTitle:"OK")
      alert.show()
    }
    UIApplication.sharedApplication().networkActivityIndicatorVisible = false
  }
}

This method creates an asynchronous web request which, when finished, will run the completion block on the main queue. When the download is complete the property list data is extracted into an NSDictionary and then processed again into an array of PhotoRecord objects. You haven’t used an NSOperation directly here, but instead you’ve accessed the main queue using NSOperationQueue.mainQueue().

Call the new method at the end of viewDidLoad:

fetchPhotoDetails()

Next, find tableView(_:cellForRowAtIndexPath:) and replace it with the following implementation:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCellWithIdentifier("CellIdentifier", forIndexPath: indexPath) as! UITableViewCell
 
  //1
  if cell.accessoryView == nil {
    let indicator = UIActivityIndicatorView(activityIndicatorStyle: .Gray)
    cell.accessoryView = indicator
  }
  let indicator = cell.accessoryView as! UIActivityIndicatorView
 
  //2
  let photoDetails = photos[indexPath.row]
 
  //3
  cell.textLabel?.text = photoDetails.name
  cell.imageView?.image = photoDetails.image
 
  //4
  switch (photoDetails.state){
  case .Filtered:
    indicator.stopAnimating()
  case .Failed:
    indicator.stopAnimating()
    cell.textLabel?.text = "Failed to load"
  case .New, .Downloaded:
    indicator.startAnimating()
    self.startOperationsForPhotoRecord(photoDetails,indexPath:indexPath)
  }
 
  return cell
}

Take some time to read through the explanation of the commented sections below:

  1. To provide feedback to the user, create a UIActivityIndicatorView and set it as the cell’s accessory view.
  2. The data source contains instances of PhotoRecord. Fetch the right one based on the current row’s indexPath.
  3. The cell’s text label is (nearly) always the same and the image is set appropriately on the PhotoRecord as it is processed, so you can set them both here, regardless of the state of the record.
  4. Inspect the record. Set up the activity indicator and text as appropriate, and kick off the operations (not yet implemented)

You can remove the implementation of applySepiaFilter since that won’t be called any more. Add the following method to the class to start the operations:

func startOperationsForPhotoRecord(photoDetails: PhotoRecord, indexPath: NSIndexPath){
  switch (photoDetails.state) {
  case .New:
    startDownloadForRecord(photoDetails, indexPath: indexPath)
  case .Downloaded:
    startFiltrationForRecord(photoDetails, indexPath: indexPath)
  default:
    NSLog("do nothing")
  }
}

Here, you’ll pass in an instance of PhotoRecord along with its index path. Depending on the photo record’s state, you kick off either the download or filter steps.

Note: the methods for downloading and filtering images are implemented separately, as there is a possibility that while an image is being downloaded the user could scroll away, and you won’t yet have applied the image filter. So next time the user comes to the same row, you don’t need to re-download the image; you only need to apply the image filter! Efficiency rocks! :]

Now you need to implement the methods that you called in the method above. Remember that you created a custom class, PendingOperations, to keep track of operations; now you actually get to use it! Add the following methods to the class:

func startDownloadForRecord(photoDetails: PhotoRecord, indexPath: NSIndexPath){
  //1
  if let downloadOperation = pendingOperations.downloadsInProgress[indexPath] {
    return
  }
 
  //2
  let downloader = ImageDownloader(photoRecord: photoDetails)
  //3
  downloader.completionBlock = {
    if downloader.cancelled {
      return
    }
    dispatch_async(dispatch_get_main_queue(), {
      self.pendingOperations.downloadsInProgress.removeValueForKey(indexPath)
      self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
    })
  }
  //4
  pendingOperations.downloadsInProgress[indexPath] = downloader
  //5
  pendingOperations.downloadQueue.addOperation(downloader)
}
 
func startFiltrationForRecord(photoDetails: PhotoRecord, indexPath: NSIndexPath){
  if let filterOperation = pendingOperations.filtrationsInProgress[indexPath]{
    return
  }
 
  let filterer = ImageFiltration(photoRecord: photoDetails)
  filterer.completionBlock = {
    if filterer.cancelled {
      return
    }
    dispatch_async(dispatch_get_main_queue(), {
      self.pendingOperations.filtrationsInProgress.removeValueForKey(indexPath)
      self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
      })
  }
  pendingOperations.filtrationsInProgress[indexPath] = filterer
  pendingOperations.filtrationQueue.addOperation(filterer)
}

Okay! Here’s a quick list to make sure you understand what’s going on in the code above:

  1. First, check for the particular indexPath to see if there is already an operation in downloadsInProgress for it. If so, ignore it.
  2. If not, create an instance of ImageDownloader by using the designated initializer.
  3. Add a completion block which will be executed when the operation is completed. This is a great place to let the rest of your app know that an operation has finished. It’s important to note that the completion block is executed even if the operation is cancelled, so you must check this property before doing anything. You also have no guarantee of which thread the completion block is called on, so you need to use GCD to trigger a reload of the table view on the main thread.
  4. Add the operation to downloadsInProgress to help keep track of things.
  5. Add the operation to the download queue. This is how you actually get these operations to start running – the queue takes care of the scheduling for you once you’ve added the operation.

The method to filter the image follows the same pattern, except it uses ImageFiltration and filtrationsInProgress to track the operations. As an exercise, you could try getting rid of the repetition in this section of code :]

You made it! Your project is complete. Build and run to see your improvements in action! As you scroll through the table view, the app doesn’t stall anymore, and starts downloading images and filtering them as they become visible.

Classic photos, now with actual scrolling!

Classic photos, now with actual scrolling!

Isn’t that cool? You can see how a little effort can go a long way towards making your applications a lot more responsive — and a lot more fun for the user!

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 want to be a great programmer, not just a good one!

You may have noticed that as you scroll away in table view, those offscreen 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! :]

Go back to Xcode, and open ListViewController.swift. Go to the implementation of tableView(_:cellForRowAtIndexPath:), and wrap the call to startOperationsForPhotoRecord in an if-clause as follows:

if (!tableView.dragging && !tableView.decelerating) {
  self.startOperationsForPhotoRecord(photoDetails, indexPath: 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, you 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 walkthrough 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 offscreen cells, and start operations for onscreen 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.suspended = true
  pendingOperations.filtrationQueue.suspended = true
}
 
func resumeAllOperations () {
  pendingOperations.downloadQueue.suspended = false
  pendingOperations.filtrationQueue.suspended = false
}
 
func loadImagesForOnscreenCells () {
  //1
  if let pathsArray = tableView.indexPathsForVisibleRows() {
    //2
    var allPendingOperations = Set(pendingOperations.downloadsInProgress.keys.array)
    allPendingOperations.unionInPlace(pendingOperations.filtrationsInProgress.keys.array)
 
    //3
    var toBeCancelled = allPendingOperations
    let visiblePaths = Set(pathsArray as! [NSIndexPath])
    toBeCancelled.subtractInPlace(visiblePaths)
 
    //4
    var toBeStarted = visiblePaths
    toBeStarted.subtractInPlace(allPendingOperations)
 
    // 5
    for indexPath in toBeCancelled {
      if let pendingDownload = pendingOperations.downloadsInProgress[indexPath] {
        pendingDownload.cancel()
      }
      pendingOperations.downloadsInProgress.removeValueForKey(indexPath)
      if let pendingFiltration = pendingOperations.filtrationsInProgress[indexPath] {
        pendingFiltration.cancel()
      }
      pendingOperations.filtrationsInProgress.removeValueForKey(indexPath)
    }
 
    // 6
    for indexPath in toBeStarted {
      let indexPath = indexPath as NSIndexPath
      let recordToProcess = self.photos[indexPath.row]
      startOperationsForPhotoRecord(recordToProcess, indexPath: indexPath)
    }
  }
}

suspendAllOperations and resumeAllOperations have a straightforward implementation. NSOperationQueues 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 + 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 startOperationsForPhotoRecord 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?

Here is the completed version of the project.

If you completed this project and took the time to really understand it, congratulations! You can consider yourself a much more valuable iOS developer than you were at the beginning of this tutorial! Most development shops are lucky to have one or two people that really know this stuff.

But beware — like deeply-nested blocks, gratuitous use of multithreading 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 it depends on are all finished. For example:

// MyDownloadOperation is a subclass of NSOperation
let downloadOperation = MyDownloadOperation()
// MyFilterOperation  is a subclass of NSOperation
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 NSOperations in general, please join the forum discussion below!

Richard Turton

Richard is an iOS developer for MartianCraft, prolific Stack Overflow participant and author of a development blog, Command Shift. When he's not in front of a computer he is usually building Lego horse powered spaceships (don't ask!) with his daughter.

User Comments

38 Comments

[ 1 , 2 , 3 ]
  • Hi there, i am so grateful for the all info in this site. I have used this method in my project but i want images to be at the center of cell but i can't accomplish it. I tried to do it on storyboard but couldn't do that. I want give images width and height and auto-layout them programmatically. Is there any way to do this?

    And i also want to use different plists on same tableview based on which button triggered the segue to tableview controller. i accomplished to detect which button activated the segue but i cant use more than one datasource(plist). For example: if user clicks "animals" button, i want to use 1plist(which have animal pictures urls in it), and if user clicks cities, i want to use 2plist(which have city pictures urls in it).

    I know it is too much to ask but i couldn't done it by myself and couldn't find it on the net.
    Thank you so much for all.
    ysnzlcn
  • For anyone trying to run the example project under iOS 9 or greater, the new App Transport Security is a feature for connection security that has default connection requirements. Wihout overriding the default behaviour, the App will trap with the following exception:

    2015-10-19 23:23:04.190 ClassicPhotos[28377:6304125] App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecure. Temporary exceptions can be configured via your app's Info.plist file.

    You can specify exceptions to the default behavior in the Info.plist file in your app or extension:

    App Transport Secuirty Override.png
    dcoughlin
  • Looking at the ImageDownloader class where you do the actual downloading:

    // 5
    let imageData = NSData(contentsOfURL:self.photoRecord.url)

    This is fairly easy and straightforward as it's synchronous.

    How would you go about something more complex, where you would be forced to use NSURLSession, which by default should be used asynchronously and a completion handler on top?

    From what I can see, your downloader.completionBlock = { ... } would/could end before NSURLSession completionhandler has been fully processed.

    Adding dispatch_semaphore_t can actually help to wait for the NSURLSession completionhandler to finish, but I'm not sure that the self.cancelled is going to work properly anymore.

    In my case I'm making a API call to a server which takes a few seconds to return. If I cancelalloperations() on my operationqueue during that short amount of time and do an if(self.cancelled) after the NSURLSession block, the return value is never true.

    So I'm wondering what the magic ingredient would be here, as I'm pulling my teeth on this one...

    Thanks and cheers!
    globi
  • globi wrote:How would you go about something more complex, where you would be forced to use NSURLSession, which by default should be used asynchronously and a completion handler on top?

    From what I can see, your downloader.completionBlock = { ... } would/could end before NSURLSession completionhandler has been fully processed.


    That's a good question. But not that complex though.

    You may wish to take a look, for example, at the downloadTaskWithURL(_:completionHandler:) method of NSURLSession class. With a simple wrapper you can perform that task as well as your NSOperation. Also check out the WWDC 2015 Advanced NSOperations section and discover it's project's source code (you can find the link under Resources tab), there are plenty of useful things!

    Hope your teeth will stay intact. =)
    Good luck!
    EvilCartman
  • Hi Richard! Thanks for the tutorial.

    I am trying to use the NSOperationQueue to process the sample buffer that can be extracted from the video feed. At the moment the video feed itself is initialised using the GCD, but I was hoping to simply just the samplebuffer processing bit in an NSOperationQueue.

    My code runs, but when testing using my phone XCode "loses connection with the device" after the feed is up and running. I feel like this is going to be a complicated fix...

    Thanks for your help
    H
    HStro
  • Hi, Thnx for great tutorial!! I learned a lot going through it.

    But there is just this one thing i am unable to fully wrap my head around, it's how thread instances of PhotoRecord class are pushing updates back to UITableViewCell or rather Photos array of TableViewController. Since there is no explicit code to update photo array i can only assume it is through some sort of reference. I would be very grateful if you can explain this concept to me in detail.

    Regards,
    mayank4396
  • @mayank4396, the reason the TableViewCell is able to access the updated photo data is that the startOperationsForPhotoRecord call is passed in a PhotoRecord object, which is stored in the photos array. This is an object, not a struct, so it's passed by reference.

    So the startDownloadForRecord call has a reference to this object, which is passes to the ImageDownloader object. This, in turn, loads up the update image, and stores that data as a UIImage that's attached to the photoRecord object. Because this is all being passed by reference, it means that photoRecord contained in the original photo array now has the UIImage as well.

    Therefore, when the ListVC reloads the table cell, it can grab the updated data from its photos object, and it's getting the newly downloaded photo.
    toddarooski
  • To convert from Swift 1.2 to Swift 2.0, I made the following changes to make this program work.

    1. Error: 'array' is unavailable: please construct an Array from your lazy sequence: Array(...)
    In the:
    func loadImagesForOnscreenCells ()

    Converting from:
    var allPendingOperations = Set(pendingOperations.downloadsInProgress.keys.array)
    allPendingOperations.unionInPlace(pendingOperations.filtrationsInProgress.keys.array)

    To:
    var allPendingOperations = Set(Array(pendingOperations.downloadsInProgress.keys))
    allPendingOperations.unionInPlace(Array(pendingOperations.filtrationsInProgress.keys))

    2. Cannot convert value of type 'Int' to expected argument type 'NSPropertyListReadOptions' (aka 'NSPropertyListMutabilityOptions')

    In the:
    func fetchPhotoDetails()

    Converting from:
    let datasourceDictionary = NSPropertyListSerialization.propertyListWithData(data!, options: Int(NSPropertyListMutabilityOptions.Immutable.rawValue), format: nil) as! NSDictionary

    to:
    let datasourceDictionary = (try! NSPropertyListSerialization.propertyListWithData(data!, options: NSPropertyListMutabilityOptions.MutableContainersAndLeaves, format: nil)) as! NSDictionary
    Thinh Le
[ 1 , 2 , 3 ]

Other Items of Interest

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Unity Starter Kit!

We are considering writing a new starter kit on making a game in C# with Unity.

Would this be something you'd be interested in, and if so which option would you prefer?

    Loading ... Loading ...

Our Books

Our Team

Video Team

... 12 total!

Swift Team

... 13 total!

iOS Team

... 52 total!

Android Team

... 9 total!

OS X Team

... 11 total!

Sprite Kit Team

... 10 total!

Unity Team

... 9 total!

Articles Team

... 11 total!