From: http://www.raywenderlich.com/19788/how-to-use-nsoperations-and-nsoperationqueues
This is a blog post by Soheil Moayedi Azarpour, an independent iOS developer.
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.
Background
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:
- 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 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.)
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.
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:
- 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 and NSOperationQueue add a little extra overhead compared to GCD, but you can add dependency among various operations. You can re-use operations, cancel or suspend them. NSOperation is compatible with Key-Value Observation (KVO); for example, you can have an NSOperation start running by listening to NSNotificationCenter.
Preliminary Project Model
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:
Implementation – the way you might first think of doing it…
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 click Next. 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 Phases and 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 to ListViewController.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:
- Import UIKit and Core Image.
- For convenience, define kDatasourceURLString as the string URL of where the datasource file is located.
- Make ListViewController a subclass of UITableViewController, by substituting NSObject to UITableViewController.
- Declare an instance of NSDictionary. This will be the data source.
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:
- Synthesize photos.
- Use lazy instantiation to load data source, i.e. photos dictionary.
- Give your view a title.
- Set the height of rows in table view to 80.0 points.
- Set photos to nil when ListViewController is unloaded.
- Return the number of rows to be displayed.
- This is an optional UITableViewDelegate method. For better viewing, change the height of each row to 80.0. The default value is 44.0.
- Get the key from the dictionary, create the NSURL from value of the key, and download the image as NSData.
- If you have successfully downloaded the data, create the image, and apply sepia filter.
- This method applies sepia filter to the image. If you would like to know more about Core Image filters, you can read Beginning Core Image in iOS 5 Tutorial.
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!
Threads
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:
- Each thread has equal access to all of your application’s resources; this includes access to any object except local variables. Therefore, any object can potentially be modified, used, and changed by any thread.
- There is no way to predict how long a thread will run — or which thread will finish first!
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.
- Race Condition: the fact that every thread can access the same memory may cause what is known as a race condition.
When multiple concurrent threads access shared data, the thread that gets to the memory first will change the shared data — and there’s no guarantee which thread will get there first. You might assume a local variable has the value your thread last wrote to this shared memory, but another thread may have changed the shared memory in the meantime, and your local variable is out of date!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.
- Atomicity:you have likely seen “nonatomic” in property declarations numerous times. When you declare a property as atomic, you usually wrap it in a @synchronized block to make it thread safe. Of course, this approach does add some extra overhead. To give you an idea, here is a rough implementation of an atomic property:
// 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.
- Deadlock: a situation where a thread is blocked waiting for a condition that can never be met. For example, if two threads that are executing with synchronized code call each other, then each thread will be waiting for the other one to finish and open the lock. But this will never happen, and both threads will be deadlocked.
- Sleepy Time: this occurs when there are too many threads executing simultaneously and the system gets bogged down. NSOperationQueue has a property that you can set to tell it how many concurrent threads you want executing at the same time.
NSOperation API
The NSOperation class has a fairly easy and short declaration. To create a customized operation, follow these steps:
- Subclass NSOperation
- Override “main”
- Create an “autoreleasepool” in “main”
- Put your code within the “autoreleasepool”
There 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 sample 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 remaining reason to perform the operation. The key is to check for the isCancelled 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:
- Start:Normally, you will not override this method. Overriding “start” requires a more complex implementation, and you have to take care of properties such as isExecuting, isFinished, isConcurrent, and isReady. When you add an operation to a queue (an instance of NSOperationQueue, which will be discussed later), the queue will call “start” on the operation and that will result in some preparation and the subsequent execution of “main”.
If you call “start” on an instance of NSOperation, without adding it to a queue, the operation will run in the main loop.
- Dependency:you can make an operation dependent on other operations. Any operation can be dependent on any number of operations. When you make operation A dependent on operation B, even though you call “start” on operation A, it will not start unless operation B isFinished is true. For example:
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];
- Priority:sometimes the operation you wish to run in the background is not crucial and can be performed at a lower priority. You set the priority of an operation by using “setQueuePriority:”.
[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. See http://research.microsoft.com/en-us/um/people/mbj/Mars_Pathfinder/Mars_Pathfinder.html for further background information on this event.)
- Completion block:another useful method in NSOperation class is setCompletionBlock:. If there is something that you want to do once the operation has been completed, you can put it in a block and pass it into this method. The block will be executed on the main thread.
[filterOp setCompletionBlock: ^{ NSLog(@"Finished filtering an image."); }];
Some additional notes on working with threads:
- If you need to pass in some values and pointers to an operation, it is a good practice to create your own designated initializer:
#import <Foundation/Foundation.h> @interface MyOperation : NSOperation -(id)initWithNumber:(NSNumber *)start string:(NSString *)string; @end
- If your operation is going to have a return value or object, it’s good practice to declare delegate methods. Remember that the delegate method must return on the main thread. However, because you are subclassing NSOperation, you must cast the operation class to NSObject first. Do this as follows:
[(NSObject *)self.delegate performSelectorOnMainThread:(@selector(delegateMethod:)) withObject:object waitUntilDone:NO];
- Always check for the isCancelled property frequently. You don’t want to run a operation in the background if it is no longer required!
- You don’t usually override the “start” method. However, if you ever decide to override the “start” method, you have to take care of properties like isExecuting, isFinished, isConcurrent, and isReady. Otherwise, your operation class won’t run properly.
- Once you add an operation to a queue (an instance of NSOperationQueue), you release it (if you are not using ARC). NSOperationQueue assumes ownership of the operation, calling “start”, and releasing it when finished.
- You cannot reuse an operation. Once it is added to a queue, you give up ownership. If you want to use the same operation class again, you must create a new instance.
- A finished operation cannot be restarted.
- If you cancel an operation, it will not happen instantly. It will happen at some point in the future when someone explicitly checks for isCancelled == YES in “main”; otherwise, the operation will run until it is done.
- Whether an operation finishes successfully, unsuccessfully, or is cancelled, the value of isFinished will always be set to YES. Therefore never assume that isFinished == YES means everything went well — particularly, if there are dependencies in your code!
NSOperationQueue API
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 it debugging easier:
NSOperationQueue *myQueue = [[NSOperationQueue alloc] init]; myQueue.name = @"Download Queue"; |
- Concurrent operations:a queue is not the same thing as thread. A queue can have multiple threads. Each operation within a queue is running on its own thread. Take the example where you create one queue, and add three operations to it. The queue will launch three separate threads, and run all operations concurrently on their own threads.
How many threads will be created? That’s a good question! :] It depends on the hardware. 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.
- Maximum number of concurrent operations:you can set the maximum number of operations that NSOperationQueue can run concurrently. NSOperationQueue may choose to run any number of concurrent operations, but it won’t be more than the maximum.
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;
- Add operation:as soon as an operation is added to a queue, you should relinquish ownership by sending a release message to the operation object (if using manual reference counting, no ARC), and the queue will then assume responsibility to start the operation. At this point, it is up to the queue as to when it will call “start”.
[myQueue addOperation:downloadOp]; [downloadOp release]; // manual reference counting
- Pending operations:at any time you can ask a queue which operations are in the queue, and how many operations there are in total. Remember that only those operations that are waiting to be executed, and those that are running, are kept in the queue. As soon as an operation is done, it is gone from the queue.
NSArray *active_and_pending_operations = myQueue.operations; NSInteger count_of_operations = myQueue.operationCount;
- Pause (suspend) queue:you can pause a queue by setting setSuspended:YES. This will suspend all operations in a queue — you can’t suspend operations individually. To resume the queue, simply setSuspended:NO.
// Suspend a queue [myQueue setSuspended:YES]; . . . // Resume a queue [myQueue setSuspended: NO];
- Cancel operations:to cancel all operations in a queue, you simply call “cancelAllOperations”. Do you remember earlier where it was noted that your code should frequently check for isCancelled property in NSOperation?
The reason is that “cancelAllOperations” doesn’t do much, except that it 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];
- addOperationWithBlock:if you have a simple operation that does not need to be subclassed, you can simply pass it into a queue by way of a block. If you want to send any data back from the block, remember that you should not pass into the block any strong reference pointers; instead, you must use a weak reference. Also, if you want to do something that is related to the UI in the block, you must do it on the main thread:
UIImage *myImage = nil; // Create a weak reference __weak UIImage *myImage_weak = myImage; // Add an operation as a block to a queue [myQueue addOperationWithBlock: ^ { // a block of operation NSURL *aURL = [NSURL URLWithString:@"http://www.somewhere.com/image.png"]; NSError *error = nil; NSData *data = [NSData dataWithContentsOfURL:aURL options:nil error:&error]; If (!error) [myImage_weak imageWithData:data]; // Get hold of main queue (main thread) [[NSOperationQueue mainQueue] addOperationWithBlock: ^ { myImageView.image = myImage_weak; // updating UI }]; }];
Redefined 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.
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 named PhotoRecord. 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:
- Import PhotoRecord.h so that you can independently set the image property of a PhotoRecord once it is successfully downloaded. If downloading fails, set its failed value to YES.
- Declare a delegate so that you can notify the caller once the operation is finished.
- Declare indexPathInTableView for convenience so that once the operation is finished, the caller has a reference to where this operation belongs to.
- Declare a designated initializer.
- In your delegate method, pass the whole class as an object back to the caller so that the caller can access both indexPathInTableView and photoRecord. Because you need to cast the operation to NSObject and return it on the main thread, the delegate method can’t have more than one argument.
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:
- Declare a private interface, so you can change the attributes of instance variables to read-write.
- Set the properties.
- Regularly check for isCancelled, to make sure the operation terminates as soon as possible.
- Apple recommends using @autoreleasepool block instead of alloc and init NSAutoreleasePool, because blocks are more efficient. You might use NSAuoreleasePool instead and that would be fine.
- Cast the operation to NSObject, and notify the caller on the main thread.
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:
- Since you need to perform filtering on the UIImage instance, you need to import both UIKit and CoreImage frameworks. You also need to import PhotoRecord. Similar to ImageDownloader, you want the caller to alloc and init using the designated initializer.
- Declare a delegate to notify the caller once its operation is finished.
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. Downloading the data source, a property list which is only about 4 kBytes, is not a big deal, and you don’t really need to bother building a subclass for it. However, 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, particularly if all you want to do is simply download a small property list. 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”! Yes, you are using ARC, but AFNetworking has not yet crawled out of the prehistoric sludge of manual memory management.
If you follow the install directions you’ll avoid compile errors, but if you don’t, there will be a LOT of errors that you’ll need to deal with at compile time. Every AFNetworking module needs “-fno-objc-arc” in your Target’s Build Phases tab, under the Compiler Flags section.
To do this, click on “PhotoRecords” in the navigation pane (on the left hand side). In the right hand side, select “ClassicPhotos” under “Targets”. From the tabs, select “Build Phases”. Below that, click on the triangle to expand “Compile Sources”. Highlight all files that belong to AFNetworking. Hit Enter, and a dialog box will up. In the dialog box, type “fno-objc-arc”, and click “Done”.
Switch to ListViewController.m 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:
- You can delete CoreImage from ListViewController header, since you don’t need it anymore. However, you do need to import PhotoRecord.h, PendingOperations.h, ImageDownloader.h and ImageFiltration.h.
- Here’s the reference to the AFNetworking library.
- Make sure to make ListViewController compliant to ImageDownloader and ImageFiltration delegate methods.
- You don’t need the data source as-is. You are going to create instances of PhotoRecord using the property list. So, change the class of “photos” from NSDictionary to NSMutableArray, so that you can update the array of photos.
- This property is used to track pending operations.
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:
- Create a NSURL and a NSURLRequest to points to the location of the data source.
- Use AFHTTPRequestOperation class, alloc and init it with the request.
- Give the user feedback, while downloading the data source by enabling network activity indicator.
- By using setCompletionBlockWithSuccess:failure:, you can add two blocks: one for the case where the operation finishes successfully, and one for the case where it fails.
- In the success block, download the property list as NSData, and then by using toll-free bridging for data into CFDataRef and CFPropertyList, convert it into NSDictionary.
- Create a NSMutableArray and iterate through all objects and keys in the dictionary, create a PhotoRecord instance, and store it in the array.
- Once you are done, point the _photo to the array of records, reload the table view and stop the network activity indicator. You also release the “plist” instance variable.
- In case you are not successful, you display a message to notify the user.
- Finally, add “datasource_download_operation” to “downloadQueue” of PendingOperations.
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:
- To provide feedback to the user, create a UIActivityIndicatorView and set it as the cell’s accessory view.
- The data source contains instances of PhotoRecord. Get a hold of each of them based on the indexPath of the row.
- Inspect the PhotoRecord. If its image is downloaded, display the image, the image name, and stop the activity indicator.
- If downloading the image has failed, display a placeholder to display the failure, and stop the activity indicator.
- Otherwise, the image has not been downloaded yet. Start the download and filtering operations (they’re not yet implemented), and display a placeholder that indicates you are working on it. Start the activity indicator to show user something is going on.
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) { // 3 [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:
- To keep it simple, you pass in an instance of PhotoRecord that requires operations, along with its indexPath.
- You inspect it to see whether it has an image; if so, then ignore it.
- If it does not have an image, start downloading the image by calling startImageDownloadingForRecord:atIndexPath: (which will be implemented shortly). You’ll do the same for filtering operations: if the image has not yet been filtered, call startImageFiltrationForRecord:atIndexPath: (which will also be implemented shortly).
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 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.
- First, check for the particular indexPath to see if there is already an operation in downloadsInProgress for it. If so, ignore it.
- If not, create an instance of ImageDownloader by using the designated initializer, and set ListViewController as the delegate. Pass in the appropriate indexPath and a pointer to the instance of PhotoRecord, and then add it to the download queue. You also add it to downloadsInProgress to help keep track of things.
- Similarly, check to see if there is any filtering operations going on for the particular indexPath.
- If not, start one by using the designated initializer.
- This one is a little tricky. You first must check to see if this particular indexPath has a pending download; if so, you make this filtering operation dependent on that. Otherwise, you don’t need dependency.
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 PhotoRecord *theRecord = downloader.photoRecord; // 3 [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; // 4 [self.pendingOperations.downloadsInProgress removeObjectForKey:indexPath]; } - (void)imageFiltrationDidFinish:(ImageFiltration *)filtration { NSIndexPath *indexPath = filtration.indexPathInTableView; PhotoRecord *theRecord = filtration.photoRecord; [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:
- Check for the indexPath of the operation, whether it is a download, or filtration.
- Get hold of the PhotoRecord instance.
- Update UI.
- Remove the operation from downloadsInProgress (or filtrationsInProgress).
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.
Kudos!
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!
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. 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:
- 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.
- If the value of decelerate is NO, 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.
- This delegate method tells you that table view stopped scrolling, so you will do the same as in #2.
OK! Now, add the implementation of suspendAllOperations, resumeAllOperations, loadImagesForOnscreenCells to the very end of ListViewController.m:
#pragma mark - #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:
- Get a set of visible rows.
- Get a set of all pending operations (download and filtration).
- Rows (or indexPaths) that need an operation = visible rows – pendings.
- Rows (or indexPaths) that their operations should be cancelled = pendings – visible rows.
- Loop through those to be cancelled, cancel them, and remove their reference from PendingOperations.
- Loop through those to be started, and call startOperationsForPhotoRecord:atIndexPath: for each.
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!
Where To Go From Here?
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.