Update 10/7/14: This tutorial has now been updated for iOS 8 and Swift; check it out!
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.
Lucky you — you get to stare at the hourglass or the colorful wheel rotating for a while until you’re able to interact with the UI again! Annoying, isn’t it?
In a mobile iOS application, users expect your apps to respond immediately to their touches, and when it doesn’t the app feels clunky and slow, and usually results in bad reviews.
However this 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? One 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 first create an app that doesn’t use multithreading 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!
Before reading this tutorial, it might be helpful to read our Multithreading and Grand Central Dispatch on iOS for Beginners Tutorial first. However, this tutorial is self-sufficient so it is not required reading.
Before you dive into the tutorial, there are a few technical concepts that need to be dealt with first.
It’s likely that you’ve heard of concurrent and parallel operations. From a technical standpoint,concurrency is a property of the program and parallel execution is a property of the machine. Parallelism and concurrency are two separate concepts. As a programmer, you can never guarantee that your code will run on a machine which is capable of processing your code in parallel operations. However, you can design your code so that it takes advantage of concurrent operations.
First, it’s imperative to define a few terms:
Note: in iPhone and Mac, 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:
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.)
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.
Before Mac OS X v10.6 and iOS 4, NSOperation and NSOperationQueue were different from GCD and used two completely different mechanisms. Starting with Mac OS X v10.6 and iOS 4, NSOperation and NSOperationQueue were built on top of GCD. As a very general rule, Apple recommends using 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 and NSOperationQueue:
In the preliminary model of the project, you have a table view with a dictionary as its data source. The keys of the dictionary are the images’ names, and the value of the each key is a URL where the image is located. The goal of this project is to read the contents of the dictionary, download images, apply an image filter, and finally display the images in a table view.
Here’s a schematic view of the model:
Note: if you don’t want to build the non-threaded first version of this project, and get right to the multithreading aspect, you can skip this section and download the first version of the project that we create in this section.
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.
Start up Xcode and create a new project with the iOS\Application\Empty Application template, and clickNext. Name it ClassicPhotos. Choose Universal, check Use Automatic Reference Counting (but nothing else is checked) and click Next. Save it wherever you like.
Select the ClassicPhoto project from the Project Navigator. Select Targets\ ClassicPhotos\Build Phasesand expand Link Binary with Libraries. Use the + button and add Core Image framework (you’ll need Core Image for image filtering).
Switch to AppDelegate.h in the Project Navigator, and import ListViewController — this is going to be the root view controller, which you’ll declare later. ListViewController will be a subclass of UITableViewController.
#import "ListViewController.h" |
Switch to AppDelegate.m, find application:didFinishLaunchingWithOptions:. Init and alloc an instance of ListViewController. Wrap it in an instance of UINavigationController and set it as the root view controller of UIWindow.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; self.window.backgroundColor = [UIColor whiteColor]; /* ListViewController is a subclass of UITableViewController. We will display images in ListViewController. Here, we wrap our ListViewController in a UINavigationController, and set it as the root view controller. */ ListViewController *listViewController = [[ListViewController alloc] initWithStyle:UITableViewStylePlain]; UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:listViewController]; self.window.rootViewController = navController; [self.window makeKeyAndVisible]; return YES; } |
Note: If you haven’t created a user interface in this way before, this is how you can create a user interface programmatically without using Storyboards or Interface Builder. We’re doing it this way just for simplicity in this tutorial.
Next create a new subclass of UITableViewController and name it ListViewController. Switch toListViewController.h and modify it as follows:
// 1 #import <UIKit/UIKit.h> #import <CoreImage/CoreImage.h> // 2 #define kDatasourceURLString @"http://www.raywenderlich.com/downloads/ClassicPhotosDictionary.plist" // 3 @interface ListViewController : UITableViewController // 4 @property (nonatomic, strong) NSDictionary *photos; // main data source of controller @end |
Let’s go through the above code section by section:
Now, switch to ListViewController.m, and add the following:
@implementation ListViewController // 1 @synthesize photos = _photos; #pragma mark - #pragma mark - Lazy instantiation // 2 - (NSDictionary *)photos { if (!_photos) { NSURL *dataSourceURL = [NSURL URLWithString:kDatasourceURLString]; _photos = [[NSDictionary alloc] initWithContentsOfURL:dataSourceURL]; } return _photos; } #pragma mark - #pragma mark - Life cycle - (void)viewDidLoad { // 3 self.title = @"Classic Photos"; // 4 self.tableView.rowHeight = 80.0; [super viewDidLoad]; } - (void)viewDidUnload { // 5 [self setPhotos:nil]; [super viewDidUnload]; } #pragma mark - #pragma mark - UITableView data source and delegate methods // 6 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { NSInteger count = self.photos.count; return count; } // 7 - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { return 80.0; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *kCellIdentifier = @"Cell Identifier"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellIdentifier]; if (!cell) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kCellIdentifier]; cell.selectionStyle = UITableViewCellSelectionStyleNone; } // 8 NSString *rowKey = [[self.photos allKeys] objectAtIndex:indexPath.row]; NSURL *imageURL = [NSURL URLWithString:[self.photos objectForKey:rowKey]]; NSData *imageData = [NSData dataWithContentsOfURL:imageURL]; UIImage *image = nil; // 9 if (imageData) { UIImage *unfiltered_image = [UIImage imageWithData:imageData]; image = [self applySepiaFilterToImage:unfiltered_image]; } cell.textLabel.text = rowKey; cell.imageView.image = image; return cell; } #pragma mark - #pragma mark - Image filtration // 10 - (UIImage *)applySepiaFilterToImage:(UIImage *)image { CIImage *inputImage = [CIImage imageWithData:UIImagePNGRepresentation(image)]; UIImage *sepiaImage = nil; CIContext *context = [CIContext contextWithOptions:nil]; CIFilter *filter = [CIFilter filterWithName:@"CISepiaTone" keysAndValues: kCIInputImageKey, inputImage, @"inputIntensity", [NSNumber numberWithFloat:0.8], nil]; CIImage *outputImage = [filter outputImage]; CGImageRef outputImageRef = [context createCGImage:outputImage fromRect:[outputImage extent]]; sepiaImage = [UIImage imageWithCGImage:outputImageRef]; CGImageRelease(outputImageRef); return sepiaImage; } @end |
Okay! There’s a lot going on here. Don’t panic – the code is explained below:
That’s it! Give it a try! Build and run. Beautiful, sepia images – but…they…appear…so…slowly! It’s nice to look at, but only if you go make a snack while you wait for it to load. :]
It’s time to think about how can you improve that user experience!
Every application has at least one thread known as the main thread. A thread’s job is to execute a sequence of instructions. In Cocoa Touch, the main thread contains the application’s main run loop. Nearly all application code that you write gets executed on the main thread, unless you specifically create a separate thread and execute some code in the new thread.
Threads have two specific characteristics:
Therefore, it is important to be aware of techniques to overcome these issues, and prevent unexpected errors! :] This is a brief list of the challenges that multi-threaded applications face — and some tips on how to deal with them effectively.
If you know that this condition might exist in your code (i.e. you know that you are going to read / write data concurrently from multiple threads) you should use mutex lock. Mutex stands for “mutual exclusion”. You create a mutex lock for instance variables by wrapping it around a “@synchronized block”. This way you make sure that the code within it is accessed only by one thread at a time:
@synchronized (self) { myClass.object = value; } |
“Self” in the above code is called a “semaphore”. When a thread reaches this piece of code, it checks to see if any other thread is accessing “self”. If nobody else is accessing “self”, it executes the block; otherwise execution of the thread is blocked until the mutex lock becomes available.
// If you declare a property as atomic ... @property (atomic, retain) NSString *myString; // ... a rough implementation that the system generates automatically, // looks like this: - (NSString *)myString { @synchronized (self) { return [[myString retain] autorelease]; } } |
In this code, “retain” and “autorelease” calls are used as the returned value is being accessed from multiple threads, and you do not want the object to get deallocated between calls.
Therefore, you retain the value first, and then put it in an autorelease pool. You can read more in Apple’s documentation about Thread Safety. This is worth knowing, if only for the reason that most iOS programmers never bother to find this out. Protip: this makes a great job interview question! :]
Most of the UIKit properties are not thread-safe. To find out whether a class is thread-safe or not, take a look at the API documentation. If the API documentation does not say anything about thread-safety, then you should assume that the class is not thread-safe.
As a general rule, if you are executing on a secondary thread and you must do something to a UIKit object, use performSelectorOnMainThread.
The NSOperation class has a fairly easy and short declaration. To create a customized operation, follow these steps:
The reason you should create your own autorelease pool is that you do not have access to the autorelease pool of the main thread, so you should create your own. Here is an example:
#import <Foundation/Foundation.h> @interface MyLengthyOperation: NSOperation @end |
@implementation MyLengthyOperation - (void)main { // a lengthy operation @autoreleasepool { for (int i = 0 ; i < 10000 ; i++) { NSLog(@"%f", sqrt(i)); } } } @end |
The code above shows the ARC syntax for autorelease pool usage. You should definitely be using ARC by now! :]
In threaded operations, you never know exactly when the operation is going to start, and how long it will take to finish. Most of the time you don’t want to perform an operation in the background if the user has scrolled away or has left a page — there’s no reason to perform the operation. The key is to check forisCancelled property of NSOperation class frequently. For example, in the imaginary sample code above, you would do this:
@interface MyLengthyOperation: NSOperation @end @implementation MyLengthyOperation - (void)main { // a lengthy operation @autoreleasepool { for (int i = 0 ; i < 10000 ; i++) { // is this operation cancelled? if (self.isCancelled) break; NSLog(@"%f", sqrt(i)); } } } @end |
To cancel an operation, you call the NSOperation’s cancel method, as shown:
// In your controller class, you create the NSOperation // Create the operation MyLengthyOperation *my_lengthy_operation = [[MyLengthyOperation alloc] init]; . . . // Cancel it [my_lengthy_operation cancel]; |
NSOperation class has a few other methods and properties:
If you call “start” on an instance of NSOperation, without adding it to a queue, the operation will run in the main loop.
MyDownloadOperation *downloadOp = [[MyDownloadOperation alloc] init]; // MyDownloadOperation is a subclass of NSOperation MyFilterOperation *filterOp = [[MyFilterOperation alloc] init]; // MyFilterOperation is a subclass of NSOperation [filterOp addDependency:downloadOp]; |
To remove dependencies:
[filterOp removeDependency:downloadOp]; |
[filterOp setQueuePriority:NSOperationQueuePriorityVeryLow]; |
Other options for thread priority are: NSOperationQueuePriorityLow, NSOperationQueuePriorityNormal, NSOperationQueuePriorityHigh, and NSOperationQueuePriorityVeryHigh.
When you add operations to a queue, the NSOperationQueue looks through all of the operations, before calling “start” on them. Those that have higher priorities will be executed first. Operations with the same priority will be executed in order of submission to the queue (FIFO).
(Historical note: In 1997, an embedded system in the Mars Rover suffered from priority inversion, perhaps the most expensive illustration of why it is important to get priority and mutex locks right. Seehttp://research.microsoft.com/en-us/um/people/mbj/Mars_Pathfinder/Mars_Pathfinder.html for further background information on this event.)
[filterOp setCompletionBlock: ^{ NSLog(@"Finished filtering an image."); }]; |
Some additional notes on working with operations:
#import <Foundation/Foundation.h> @interface MyOperation : NSOperation -(id)initWithNumber:(NSNumber *)start string:(NSString *)string; @end |
[(NSObject *)self.delegate performSelectorOnMainThread:@selector(delegateMethod:) withObject:object waitUntilDone:NO]; |
NSOperationQueue also has a fairly simple interface. It is even simpler than NSOperation, because you don’t need to subclass it, or override any method — you simply create one. It is a good practice to give your queue a name; this way you can identify your operation queues at run time and make its debugging easier:
NSOperationQueue *myQueue = [[NSOperationQueue alloc] init]; myQueue.name = @"Download Queue"; |
By default, NSOperationQueue class will do some magic 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 NSOperationQueue 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 NSOperationQueue will only launch two simultaneous threads.
myQueue.MaxConcurrentOperationCount = 3; |
If you change your mind, and want to set MaxConcurrentOperationCount back to its default, you would perform the following changes:
myQueue.MaxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount; |
[myQueue addOperation:downloadOp]; [downloadOp release]; // manual reference counting |
NSArray *active_and_pending_operations = myQueue.operations; NSInteger count_of_operations = myQueue.operationCount; |
// Suspend a queue [myQueue setSuspended:YES]; . . . // Resume a queue [myQueue setSuspended: NO]; |
The reason is that “cancelAllOperations” calls “cancel” on every operation in the queue — it doesn’t do anything magical! :] If an operation has not yet started, and you call “cancel” on it, the operation will be cancelled and removed from the queue. However, if an operation is already executing, it is up to that individual operation to recognize the cancellation (by checking the isCancelled property) and stop what it is doing.
[myQueue cancelAllOperations]; |
// Create a weak reference __weak MyViewController *weakSelf = self; // Add an operation as a block to a queue [myQueue addOperationWithBlock: ^ { NSURL *aURL = [NSURL URLWithString:@"http://www.somewhere.com/image.png"]; NSError *error = nil; NSData *data = [NSData dataWithContentsOfURL:aURL options:nil error:&error]; UIImage *image = nil; If (data) image = [UIImage imageWithData:data]; // Update UI on the main thread. [[NSOperationQueue mainQueue] addOperationWithBlock: ^ { weakSelf.imageView.image = image; }]; }]; |
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.
Note: if you can’t immediately see why your app is running so slow — and sometimes it isn’t obvious — you should use Instruments. However, that’s another whole tutorial unto itself! :]
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 is done on the main thread. At this point, 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 once it is downloaded without waiting for the filtering process. Once the image filtering is complete, update the UI to display the filtered image. The diagram below shows the schematic control flow for this process:
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 NSObject subclass to your project namedPhotoRecord. Open PhotoRecord.h, and add the followings to the header file:
#import <UIKit/UIKit.h> // because we need UIImage @interface PhotoRecord : NSObject @property (nonatomic, strong) NSString *name; // To store the name of image @property (nonatomic, strong) UIImage *image; // To store the actual image @property (nonatomic, strong) NSURL *URL; // To store the URL of the image @property (nonatomic, readonly) BOOL hasImage; // Return YES if image is downloaded. @property (nonatomic, getter = isFiltered) BOOL filtered; // Return YES if image is sepia-filtered @property (nonatomic, getter = isFailed) BOOL failed; // Return Yes if image failed to be downloaded @end |
Does the syntax above look familiar? Each property has a getter and setter. Specifying the getter like this in the property specification merely makes the naming of the getter method explicit.
Switch to PhotoRecord.m, and add the following:
@implementation PhotoRecord @synthesize name = _name; @synthesize image = _image; @synthesize URL = _URL; @synthesize hasImage = _hasImage; @synthesize filtered = _filtered; @synthesize failed = _failed; - (BOOL)hasImage { return _image != nil; } - (BOOL)isFailed { return _failed; } - (BOOL)isFiltered { return _filtered; } @end |
To track status of each operation, you’ll need a separate class. Create another new subclass of NSObject named PendingOperations. Switch to PendingOperations.h and add the following:
#import <Foundation/Foundation.h> @interface PendingOperations : NSObject @property (nonatomic, strong) NSMutableDictionary *downloadsInProgress; @property (nonatomic, strong) NSOperationQueue *downloadQueue; @property (nonatomic, strong) NSMutableDictionary *filtrationsInProgress; @property (nonatomic, strong) NSOperationQueue *filtrationQueue; @end |
This one is also simple. You declare two dictionaries to keep track of active and pending downloads and filtering. The dictionary keys reference the indexPath of table view rows, and the dictionary values are going to be the separate instances of ImageDownloader and ImageFiltration.
Note:You might wonder why you have to keep track of all active and pending operations. Isn’t it possible to simply access them by making an inquiry to [NSOperationQueue operations]? Well, yes, but in this project it won’t be very efficient to do so.
Every time that you need to compare the indexPath of visible rows with the indexPath of rows that have a pending operation, you would need to use several iterative loops, which is an expensive operation. By declaring an extra instance of NSDictionary, you can conveniently keep track of pending operations without the need to perform inefficient loop operations.
Switch to PendingOperations.m and add the following:
@implementation PendingOperations @synthesize downloadsInProgress = _downloadsInProgress; @synthesize downloadQueue = _downloadQueue; @synthesize filtrationsInProgress = _filtrationsInProgress; @synthesize filtrationQueue = _filtrationQueue; - (NSMutableDictionary *)downloadsInProgress { if (!_downloadsInProgress) { _downloadsInProgress = [[NSMutableDictionary alloc] init]; } return _downloadsInProgress; } - (NSOperationQueue *)downloadQueue { if (!_downloadQueue) { _downloadQueue = [[NSOperationQueue alloc] init]; _downloadQueue.name = @"Download Queue"; _downloadQueue.maxConcurrentOperationCount = 1; } return _downloadQueue; } - (NSMutableDictionary *)filtrationsInProgress { if (!_filtrationsInProgress) { _filtrationsInProgress = [[NSMutableDictionary alloc] init]; } return _filtrationsInProgress; } - (NSOperationQueue *)filtrationQueue { if (!_filtrationQueue) { _filtrationQueue = [[NSOperationQueue alloc] init]; _filtrationQueue.name = @"Image Filtration Queue"; _filtrationQueue.maxConcurrentOperationCount = 1; } return _filtrationQueue; } @end |
Here, you override some getters to take advantage of lazy instantiation, so that you don’t actually allocate instance variables until they are accessed. You also initialize and allocate two queues — one for downloading operations, one for filtering — and set their properties, so that when you access them in another class, you don’t have to worry about their initialization. The maxConcurrentOperationCount is set to 1 here for the sake of this tutorial.
Now, it’s time to take care of download and filtration operations. Create a new subclass of NSOperation named ImageDownloader. Switch to ImageDownloader.h, and add the following:
#import <Foundation/Foundation.h> // 1 #import "PhotoRecord.h" // 2 @protocol ImageDownloaderDelegate; @interface ImageDownloader : NSOperation @property (nonatomic, assign) id <ImageDownloaderDelegate> delegate; // 3 @property (nonatomic, readonly, strong) NSIndexPath *indexPathInTableView; @property (nonatomic, readonly, strong) PhotoRecord *photoRecord; // 4 - (id)initWithPhotoRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath delegate:(id<ImageDownloaderDelegate>) theDelegate; @end @protocol ImageDownloaderDelegate <NSObject> // 5 - (void)imageDownloaderDidFinish:(ImageDownloader *)downloader; @end |
Here’s what’s happening at each of the numbered comments in the code above:
Switch to ImageDownloader.m and make the following changes:
// 1 @interface ImageDownloader () @property (nonatomic, readwrite, strong) NSIndexPath *indexPathInTableView; @property (nonatomic, readwrite, strong) PhotoRecord *photoRecord; @end @implementation ImageDownloader @synthesize delegate = _delegate; @synthesize indexPathInTableView = _indexPathInTableView; @synthesize photoRecord = _photoRecord; #pragma mark - #pragma mark - Life Cycle - (id)initWithPhotoRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath delegate:(id<ImageDownloaderDelegate>)theDelegate { if (self = [super init]) { // 2 self.delegate = theDelegate; self.indexPathInTableView = indexPath; self.photoRecord = record; } return self; } #pragma mark - #pragma mark - Downloading image // 3 - (void)main { // 4 @autoreleasepool { if (self.isCancelled) return; NSData *imageData = [[NSData alloc] initWithContentsOfURL:self.photoRecord.URL]; if (self.isCancelled) { imageData = nil; return; } if (imageData) { UIImage *downloadedImage = [UIImage imageWithData:imageData]; self.photoRecord.image = downloadedImage; } else { self.photoRecord.failed = YES; } imageData = nil; if (self.isCancelled) return; // 5 [(NSObject *)self.delegate performSelectorOnMainThread:@selector(imageDownloaderDidFinish:) withObject:self waitUntilDone:NO]; } } @end |
Stepping through the numbered comments, you’ll see that the code does the following:
Now, go ahead and create a subclass of NSOperation to take care of image filtering!
Create another new subclass of NSOperation named ImageFiltration. Open ImageFiltration.h, and add the following:
// 1 #import <UIKit/UIKit.h> #import <CoreImage/CoreImage.h> #import "PhotoRecord.h" // 2 @protocol ImageFiltrationDelegate; @interface ImageFiltration : NSOperation @property (nonatomic, weak) id <ImageFiltrationDelegate> delegate; @property (nonatomic, readonly, strong) NSIndexPath *indexPathInTableView; @property (nonatomic, readonly, strong) PhotoRecord *photoRecord; - (id)initWithPhotoRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath delegate:(id<ImageFiltrationDelegate>)theDelegate; @end @protocol ImageFiltrationDelegate <NSObject> - (void)imageFiltrationDidFinish:(ImageFiltration *)filtration; @end |
Once again, here’s what the above code is doing:
Switch to ImageFiltration.m and add the following code:
@interface ImageFiltration () @property (nonatomic, readwrite, strong) NSIndexPath *indexPathInTableView; @property (nonatomic, readwrite, strong) PhotoRecord *photoRecord; @end @implementation ImageFiltration @synthesize indexPathInTableView = _indexPathInTableView; @synthesize photoRecord = _photoRecord; @synthesize delegate = _delegate; #pragma mark - #pragma mark - Life cycle - (id)initWithPhotoRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath delegate:(id<ImageFiltrationDelegate>)theDelegate { if (self = [super init]) { self.photoRecord = record; self.indexPathInTableView = indexPath; self.delegate = theDelegate; } return self; } #pragma mark - #pragma mark - Main operation - (void)main { @autoreleasepool { if (self.isCancelled) return; if (!self.photoRecord.hasImage) return; UIImage *rawImage = self.photoRecord.image; UIImage *processedImage = [self applySepiaFilterToImage:rawImage]; if (self.isCancelled) return; if (processedImage) { self.photoRecord.image = processedImage; self.photoRecord.filtered = YES; [(NSObject *)self.delegate performSelectorOnMainThread:@selector(imageFiltrationDidFinish:) withObject:self waitUntilDone:NO]; } } } #pragma mark - #pragma mark - Filtering image - (UIImage *)applySepiaFilterToImage:(UIImage *)image { // This is expensive + time consuming CIImage *inputImage = [CIImage imageWithData:UIImagePNGRepresentation(image)]; if (self.isCancelled) return nil; UIImage *sepiaImage = nil; CIContext *context = [CIContext contextWithOptions:nil]; CIFilter *filter = [CIFilter filterWithName:@"CISepiaTone" keysAndValues: kCIInputImageKey, inputImage, @"inputIntensity", [NSNumber numberWithFloat:0.8], nil]; CIImage *outputImage = [filter outputImage]; if (self.isCancelled) return nil; // Create a CGImageRef from the context // This is an expensive + time consuming CGImageRef outputImageRef = [context createCGImage:outputImage fromRect:[outputImage extent]]; if (self.isCancelled) { CGImageRelease(outputImageRef); return nil; } sepiaImage = [UIImage imageWithCGImage:outputImageRef]; CGImageRelease(outputImageRef); return sepiaImage; } @end |
The implementation above is very similar to ImageDownloader. The image filtering is the same method implementation you used previously in ListViewController.m. It’s been moved here so that it can be done as a separate operation in the background. You should check for isCancelled very frequently; a good practice is to call it before and after any expensive method call. Once the filtering is done, the values of PhotoRecord instance are set appropriately, and then the delegate on the main thread is notified.
Great! Now you have all the tools and foundation you need in order to process operations in a background thread. It’s time to go back to the view controller and modify it appropriately, so that it can take advantage of all these new benefits.
Note: Before moving on, you’ll want to download the AFNetworking library from GitHub.
The AFNetworking library is built upon NSOperation and NSOperationQueue. It provides you with lots of convenient methods so that you don’t have to create your own operations for common tasks like downloading a file in the background.
When it comes to downloading a file from the internet, it’s good practice to have some code in place to check for errors.You can never assume that there is going to be a reliable constant internet connection.
Apple provides the NSURLConnection class for this purpose. Using that can be extra work. AFNetworking is an open source library that provides a very convenient way to do such tasks. You pass in two blocks, one for when the operation finishes successfully, and one for the time the operation fails. You will see it in action a little later on.
To add the library to your project, select File > Add Files To …, then browse and select the folder where you downloaded AFNetworking, and finally click “Add”. Make sure to tick the box “Copy items into destination group’s folder”!
Switch to ListViewController.h and update the header file as follows:
// 1 #import <UIKit/UIKit.h> // #import <CoreImage/CoreImage.h> ... you don't need CoreImage here anymore. #import "PhotoRecord.h" #import "PendingOperations.h" #import "ImageDownloader.h" #import "ImageFiltration.h" // 2 #import "AFNetworking/AFNetworking.h" #define kDatasourceURLString @"https://sites.google.com/site/soheilsstudio/tutorials/nsoperationsampleproject/ClassicPhotosDictionary.plist" // 3 @interface ListViewController : UITableViewController <ImageDownloaderDelegate, ImageFiltrationDelegate> // 4 @property (nonatomic, strong) NSMutableArray *photos; // main data source of controller // 5 @property (nonatomic, strong) PendingOperations *pendingOperations; @end |
What’s going on here? The following points explain the code above:
Switch to ListViewController.m, and update it as follows:
// Add this to the beginning of ListViewController.m @synthesize pendingOperations = _pendingOperations; . . . // Add this to viewDidUnload [self setPendingOperations:nil]; |
Right before the lazy instantiation of “photos”, add a lazy instantiation of “pendingOperations”:
- (PendingOperations *)pendingOperations { if (!_pendingOperations) { _pendingOperations = [[PendingOperations alloc] init]; } return _pendingOperations; } |
Go to the lazy instantiation of “photos” and modify it as follows:
- (NSMutableArray *)photos { if (!_photos) { // 1 NSURL *datasourceURL = [NSURL URLWithString:kDatasourceURLString]; NSURLRequest *request = [NSURLRequest requestWithURL:datasourceURL]; // 2 AFHTTPRequestOperation *datasource_download_operation = [[AFHTTPRequestOperation alloc] initWithRequest:request]; // 3 [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES]; // 4 [datasource_download_operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) { // 5 NSData *datasource_data = (NSData *)responseObject; CFPropertyListRef plist = CFPropertyListCreateFromXMLData(kCFAllocatorDefault, (__bridge CFDataRef)datasource_data, kCFPropertyListImmutable, NULL); NSDictionary *datasource_dictionary = (__bridge NSDictionary *)plist; // 6 NSMutableArray *records = [NSMutableArray array]; for (NSString *key in datasource_dictionary) { PhotoRecord *record = [[PhotoRecord alloc] init]; record.URL = [NSURL URLWithString:[datasource_dictionary objectForKey:key]]; record.name = key; [records addObject:record]; record = nil; } // 7 self.photos = records; CFRelease(plist); [self.tableView reloadData]; [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO]; } failure:^(AFHTTPRequestOperation *operation, NSError *error){ // 8 // Connection error message UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Oops!" message:error.localizedDescription delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alert show]; alert = nil; [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO]; }]; // 9 [self.pendingOperations.downloadQueue addOperation:datasource_download_operation]; } return _photos; } |
There’s a fair bit going on in the code above. Stepping through the commented sections one by one, here’s what the code above has accomplished:
Go to tableView:cellForRowAtIndexPath: and modify it as follows:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *kCellIdentifier = @"Cell Identifier"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellIdentifier]; if (!cell) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kCellIdentifier]; cell.selectionStyle = UITableViewCellSelectionStyleNone; // 1 UIActivityIndicatorView *activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; cell.accessoryView = activityIndicatorView; } // 2 PhotoRecord *aRecord = [self.photos objectAtIndex:indexPath.row]; // 3 if (aRecord.hasImage) { [((UIActivityIndicatorView *)cell.accessoryView) stopAnimating]; cell.imageView.image = aRecord.image; cell.textLabel.text = aRecord.name; } // 4 else if (aRecord.isFailed) { [((UIActivityIndicatorView *)cell.accessoryView) stopAnimating]; cell.imageView.image = [UIImage imageNamed:@"Failed.png"]; cell.textLabel.text = @"Failed to load"; } // 5 else { [((UIActivityIndicatorView *)cell.accessoryView) startAnimating]; cell.imageView.image = [UIImage imageNamed:@"Placeholder.png"]; cell.textLabel.text = @""; [self startOperationsForPhotoRecord:aRecord atIndexPath:indexPath]; } return cell; } |
Again, take some time to read through the explanation of the commented sections below:
Now it’s time to implement the method that will take care of starting operations. You can delete the old implementation of “applySepiaFilterToImage:” in ListViewController.m, if you haven’t done so already.
Go all the way to the end of your code, and implement the following method:
// 1 - (void)startOperationsForPhotoRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath { // 2 if (!record.hasImage) { [self startImageDownloadingForRecord:record atIndexPath:indexPath]; } if (!record.isFiltered) { [self startImageFiltrationForRecord:record atIndexPath:indexPath]; } } |
The above code is pretty straightforward, but here’s some explanation as to what it’s doing:
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 can 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 startImageDownloadingForRecord:atIndexPath: that you called in the code snippets above. Remember that you created a custom class, PendingOperations, to keep track of operations. Here, you actually get to use it:
- (void)startImageDownloadingForRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath { // 1 if (![self.pendingOperations.downloadsInProgress.allKeys containsObject:indexPath]) { // 2 // Start downloading ImageDownloader *imageDownloader = [[ImageDownloader alloc] initWithPhotoRecord:record atIndexPath:indexPath delegate:self]; [self.pendingOperations.downloadsInProgress setObject:imageDownloader forKey:indexPath]; [self.pendingOperations.downloadQueue addOperation:imageDownloader]; } } - (void)startImageFiltrationForRecord:(PhotoRecord *)record atIndexPath:(NSIndexPath *)indexPath { // 3 if (![self.pendingOperations.filtrationsInProgress.allKeys containsObject:indexPath]) { // 4 // Start filtration ImageFiltration *imageFiltration = [[ImageFiltration alloc] initWithPhotoRecord:record atIndexPath:indexPath delegate:self]; // 5 ImageDownloader *dependency = [self.pendingOperations.downloadsInProgress objectForKey:indexPath]; if (dependency) [imageFiltration addDependency:dependency]; [self.pendingOperations.filtrationsInProgress setObject:imageFiltration forKey:indexPath]; [self.pendingOperations.filtrationQueue addOperation:imageFiltration]; } } |
Okay! Here’s a quick list to make sure you understand what’s going on in the code above.
Great! You now need to implement the delegate methods of ImageDownloader and ImageFiltration. Add the following at the end of ListViewController.m:
- (void)imageDownloaderDidFinish:(ImageDownloader *)downloader { // 1 NSIndexPath *indexPath = downloader.indexPathInTableView; // 2 [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; // 3 [self.pendingOperations.downloadsInProgress removeObjectForKey:indexPath]; } - (void)imageFiltrationDidFinish:(ImageFiltration *)filtration { NSIndexPath *indexPath = filtration.indexPathInTableView; [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; [self.pendingOperations.filtrationsInProgress removeObjectForKey:indexPath]; } |
Both delegate methods have very similar implementations, so there’s only a need to go over one of them:
Update: “xlledo” from the forums made a good point in regard to handling instances of PhotoRecord. Because you are passing a pointer to PhotoRecord to NSOperation subclasses (ImageDownloader and ImageFiltration), you modify them directly. Therefore, replaceObjectAtIndex:withObject: is redundant and not needed.
Wow! 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.
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!
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. Didn’t you put cancellation provisions in your code? Yes, you did — you should probably make use of them! :]
Go back to Xcode, and switch to ListViewController.m. Go to the implementation of tableView:cellForRowAtIndexPath:, and wrap [self startOperationsForPhotoRecord:aRecord atIndexPath:indexPath]; in an if-clause as follows:
// in implementation of tableView:cellForRowAtIndexPath: if (!tableView.dragging && !tableView.decelerating) { [self startOperationsForPhotoRecord:aRecord atIndexPath: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.
Now, go to the end of ListViewController.m and implement the following UIScrollView delegate methods:
#pragma mark - #pragma mark - UIScrollView delegate - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { // 1 [self suspendAllOperations]; } - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { // 2 if (!decelerate) { [self loadImagesForOnscreenCells]; [self resumeAllOperations]; } } - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { // 3 [self loadImagesForOnscreenCells]; [self resumeAllOperations]; } |
A quick walkthrough of the code above shows the following:
Add the implementation of suspendAllOperations, resumeAllOperations, loadImagesForOnscreenCells to the very end of ListViewController.m:
#pragma mark - Cancelling, suspending, resuming queues / operations - (void)suspendAllOperations { [self.pendingOperations.downloadQueue setSuspended:YES]; [self.pendingOperations.filtrationQueue setSuspended:YES]; } - (void)resumeAllOperations { [self.pendingOperations.downloadQueue setSuspended:NO]; [self.pendingOperations.filtrationQueue setSuspended:NO]; } - (void)cancelAllOperations { [self.pendingOperations.downloadQueue cancelAllOperations]; [self.pendingOperations.filtrationQueue cancelAllOperations]; } - (void)loadImagesForOnscreenCells { // 1 NSSet *visibleRows = [NSSet setWithArray:[self.tableView indexPathsForVisibleRows]]; // 2 NSMutableSet *pendingOperations = [NSMutableSet setWithArray:[self.pendingOperations.downloadsInProgress allKeys]]; [pendingOperations addObjectsFromArray:[self.pendingOperations.filtrationsInProgress allKeys]]; NSMutableSet *toBeCancelled = [pendingOperations mutableCopy]; NSMutableSet *toBeStarted = [visibleRows mutableCopy]; // 3 [toBeStarted minusSet:pendingOperations]; // 4 [toBeCancelled minusSet:visibleRows]; // 5 for (NSIndexPath *anIndexPath in toBeCancelled) { ImageDownloader *pendingDownload = [self.pendingOperations.downloadsInProgress objectForKey:anIndexPath]; [pendingDownload cancel]; [self.pendingOperations.downloadsInProgress removeObjectForKey:anIndexPath]; ImageFiltration *pendingFiltration = [self.pendingOperations.filtrationsInProgress objectForKey:anIndexPath]; [pendingFiltration cancel]; [self.pendingOperations.filtrationsInProgress removeObjectForKey:anIndexPath]; } toBeCancelled = nil; // 6 for (NSIndexPath *anIndexPath in toBeStarted) { PhotoRecord *recordToProcess = [self.photos objectAtIndex:anIndexPath.row]; [self startOperationsForPhotoRecord:recordToProcess atIndexPath:anIndexPath]; } toBeStarted = nil; } |
suspendAllOperations, resumeAllOperations and cancelAllOperations have a straightforward implementation. You basically use factory methods to suspend, resume or cancel operations and queues. For convenience, you put them together in separate methods.
LoadImagesForOnscreenCells is little complex. Here’s what’s going on:
And finally, the last piece of this puzzle is solved by didReceiveMemoryWarning of ListViewController.m.
// If app receive memory warning, cancel all operations - (void)didReceiveMemoryWarning { [self cancelAllOperations]; [super didReceiveMemoryWarning]; } |
Build and run and you should have a more responsive, and better resource-managed application! Give yourself a round of applause!
Here is the complete improved 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 threads 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.
If you have any comments or questions about this tutorial or NSOperations in general, please join the forum discussion below!