This chapter covers how to add costly tasks to your applications without interrupting the main thread of operations. Objective-C supports many different options to solve this problem and this chapter covers the three most important: NSThread
, Grand Central Dispatch, and NSOperationQueue
.
The recipes in this chapter will show you how to:
NOTE: The topics in this chapter can be complex because multithreading is a difficult problem for software developers. The underlying technology that facilitates multithreading has evolved rapidly over the last few years, which is why you will see a few alternatives here that you can use to solve the problems involved with asynchronous processing.
Your application needs to execute a task that will take a long time, but you would like the user interface to remain responsive and otherwise unaffected by the new operation.
Put the long task into a method and then use NSThread
to create a thread separate from the main thread where the new operation is taking place.
We refer to executable program like applications as processes when they are being executed by the operating system (here either iOS or OSX). Processes are made up of threads in which operations are executed at the same time. These operations could have happened at the same time on different processors or on the same processor using a time-sharing strategy (each thread takes a turn using the computer’s processor).
All programs have at least one primary thread referred to as the main thread. Applications use the main thread to manage the user interface, but there may be other threads working at the same time doing tasks that are not directly related to the user interface or generally part of the main thread.
NOTE: To follow along with this recipe, you will need to have a Single View iPhone application with a button and an activity view. For general information on how to use iPhone applications and user controls, see Recipe 1.12 and Recipe 1.13. See the complete code listings here for the details on setting up the user interface.
To create a new thread in Objective-C, you can use the NSThread
class, but first you need to put all the code that you want to run on the separate thread in a method.
-(void) bigTask{
for(int i=0;i<40000;i++){
NSString *newString = [NSString stringWithFormat:@"i = %i", i];
NSLog(@"%@", newString);
}
[self.myActivityIndicator stopAnimating];
}
This method, bigTask
, loops 40,000 times. During each loop a new string is constructed and then written out to the log. After all this, a message is sent to the activity indicator to stop spinning, which indicates that the task is complete.
There is one more thing that you should do with bigTask
before you move on. It has to do with memory management, which is covered in more detail in Chapter 8, and is important when you are dealing with threads. You need to put the code in bigTask
into an autoreleasepool. Autoreleasepool allows Objective-C to use memory resources and then dispose of the resources as needed. Every thread requires an autoreleasepool
or you will find memory leaks in your app.
To add an autoreleasepool
to bigTask
, enclose all of bigTask
’s code in a block starting with the @autorelease
keyword.
-(void) bigTask{
@autoreleasepool {
for(int i=0;i<40000;i++){
NSString *newString = [NSString stringWithFormat:@"i = %i", i];
NSLog(@"%@", newString);
}
[self.myActivityIndicator stopAnimating];
}
}
You could simply execute this method directly by assigning bigTask
as an action associated with a touch event to a button. However, if you did so, your user interface would be completely unresponsive until the task was complete.
A better way to execute the bigTask
is to create a new method for the task of setting up the user interface and then executing the bigTask
. You will ultimately assign this method to a button action. Call it bitTaskAction
and start the method like this:
-(void)bigTaskAction{
[self.myActivityIndicator startAnimating];
}
So far, bigTaskAction
just sets up the user interface by sending a message to the activity indicator to start spinning. To execute the big task, use the NSThread
class method detachNewThreadSelector:toTarget:withObject:.
-(void)bigTaskAction{
[self.myActivityIndicator startAnimating];
[NSThread detachNewThreadSelector:@selector(bigTask)
toTarget:self
withObject:nil];
}
This will make a new thread and execute the code in the method that you specify with the @selector
keyword. You can also specify the target object, which must be the object where the method from the first parameter is located. If the method accepts a parameter, then you can pass an object using the last parameter in the method. Yours doesn’t require a parameter so you simply pass nil
.
To make this work with an application, assign the method that spawns the thread as an action to a user control. If you add this to an iPhone app, make sure to add bigTaskAction
to the action
method.
[self.myButton addTarget:self
action:@selector(bigTaskAction)
forControlEvents:UIControlEventTouchUpInside];
When a user touches a button on an app like this, the big task will execute in its own thread and not interrupt the user interface at all. See Listings 6-1 through 6-4 for the code.
#import <UIKit/UIKit.h>
@class ViewController;
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) ViewController *viewController;
@end
#import "AppDelegate.h"
#import "ViewController.h"
@implementation AppDelegate
@synthesize window = _window;
@synthesize viewController = _viewController;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:
(NSDictionary *)launchOptions{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
self.viewController = [[ViewController alloc] initWithNibName:@"ViewController"
bundle:nil];
self.window.rootViewController = self.viewController;
[self.window makeKeyAndVisible];
return YES;
}
@end
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@property(strong) UIButton *myButton;
@property(strong) UIActivityIndicatorView *myActivityIndicator;
@end
#import "ViewController.h"
@implementation ViewController
@synthesize myButton, myActivityIndicator;
-(void) bigTask{
@autoreleasepool {
for(int i=0;i<40000;i++){
NSString *newString = [NSString stringWithFormat:@"i = %i", i];
NSLog(@"%@", newString);
}
[self.myActivityIndicator stopAnimating];
}
}
/*
//do task without using a new thread (watch UI to see how this works)
-(void)bigTaskAction{
[self.myActivityIndicator startAnimating];
[self bigTask];
}
*/
//do task by detaching a new thread (watch UI to see how this works)
-(void)bigTaskAction{
[self.myActivityIndicator startAnimating];
[NSThread detachNewThreadSelector:@selector(bigTask)
toTarget:self
withObject:nil];
}
- (void)viewDidLoad{
[super viewDidLoad];
//Create button
self.myButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
self.myButton.frame = CGRectMake(20, 403, 280, 37);
[self.myButton addTarget:self
action:@selector(bigTaskAction)
forControlEvents:UIControlEventTouchUpInside];
[self.myButton setTitle:@"Do Long Task"
forState:UIControlStateNormal];
[self.view addSubview:self.myButton];
//Create activity indicator
self.myActivityIndicator = [[UIActivityIndicatorView alloc] init];
self.myActivityIndicator.frame = CGRectMake(142, 211, 37, 37);
self.myActivityIndicator.activityIndicatorViewStyle =
UIActivityIndicatorViewStyleWhiteLarge;
self.myActivityIndicator.hidesWhenStopped = NO;
[self.view addSubview:self.myActivityIndicator];
}
@end
To use this code, start by setting up a Single View based application in Xcode, like you did in Recipe 1.12. Next, add the code from Listing 6-4 to your own ViewController
class.
Run the application. In the iOS Simulator you should see an app with one button and a white activity indicator in the middle of the view. Touch the button and examine the log. You should see the for
loop executing and the activity indicator spinning in the iOS app view. When bigTask
is complete, the activity indicator will stop spinning.
To compare what would happen without using a new thread, comment out bigTaskAction
from Listing 6-4 and then comment in the alternate bigTask
method. See the comments in Listing 6-4 if you are unsure of what method to choose.
Run the application with the alternate bigTaskAction
and observe how the main thread gets locked up. You will not be able to use the interface, but the for
loop will continue to execute and you will see the results in the log.
When you attempt to update the user interface from a background thread, the changes don’t occur until the background thread is finished processing, which makes components like progress bars useless. You would like to update your user on the progress of background tasks.
Use the NSObject
method performSelectorOnMainThread:withObject:waitUntilDone:
to execute a method on the main thread. You will need to put the code that updates your user interface (or otherwise works with the main thread) in its own method.
This recipe continues the work you started in Recipe 6.1. However, you are going to add a UIProgressView
object property to your application.
UIProgressView
is a UIKit
class that you can use to create a user interface element that presents the progress of a task from 0% to 100% and looks like a blue bar that moves across the screen. You are going to use the progress view to show your users how much of the bigTask
has been completed.
Here is where you will put the UIProgressView
object in your ViewController
class. This property belongs in the header file.
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@property(strong) UIButton *myButton;
@property(strong) UIActivityIndicatorView *myActivityIndicator;
@property(strong) UIProgressView *myProgressView;
@end
The property myProgressView
must also be added to the @synthesize
statement in the ViewController
implementation file.
#import "ViewController.h"
@implementation ViewController
@synthesize myButton, myActivityIndicator, myProgressView;
...
@end
Note that the entire ViewController
implementation is not reproduced here. Take a look at Listing 6-7 to see a complete listing of the code in ViewController
in context.
Now, create a method that will update the progress view. This method must be separate from the method where you put the code for the background thread, which is bigTask
in this example application.
Name your method updateProgressViewWithPercentage:
. The parameter is an NSNumber
object indicating how much of the task is complete.
-(void)updateProgressViewWithPercentage:(NSNumber *)percentDone{
}
Make sure to add this method to your ViewController
implementation but before the bigTask
method. Alternatively, you can add updateProgressViewWithPercentage:
to the ViewController
header file as a forward declaration if you’d rather locate the actual method after bigTask
.
Next, add the code to update the progress view to the new method. You only need to send one message here to the progress view with the updated information.
-(void)updateProgressViewWithPercentage:(NSNumber *)percentDone{
[self.myProgressView setProgress:[percentDone floatValue]
animated:YES];
}
As you can see, you are setting the state of the progress view to the percentage that you are getting from the percentDone
parameter. setProgress
is actually expecting a primitive float type, which means that you must use the NSNumber floatValue
method to return the float
value version of the NSNumber
object. The second parameter allows you to control whether the progress view will update with animation.
This is the method that you will be calling from your background thread. As in Recipe 6.1, you have a method called bigTask
where the background thread code is located. There is a slight change in this code: the number to count to is reduced to 10,000 from 40,000 because counting to 40,000 just took too long while testing this code. Here is the updated bigTask
:
-(void) bigTask{
@autoreleasepool {
for(int i=0;i<10000;i++){
NSString *newString = [NSString stringWithFormat:@"i = %i", i];
NSLog(@"%@", newString);
}
[self.myActivityIndicator stopAnimating];
}
}
Again, the code for bigTask
is located in the ViewController
implementation. Before you instruct the user interface to update back on the main thread, you need a few things in place. Since you don’t want the interface updating for every single action in the thread, here are some rules to determine when the interface is updated.
You want the interface to update as you reach every 10% of the total task completion time. Since you are counting to 10,000, you can simply update the interface every 1,000 counts. You’ll need an integer to help keep track of this, so add it now and assign the initial value to 1000
.
-(void) bigTask{
@autoreleasepool {
int updateUIWhen = 1000;
for(int i=0;i<10000;i++){
NSString *newString = [NSString stringWithFormat:@"i = %i", i];
NSLog(@"%@", newString);
}
[self.myActivityIndicator stopAnimating];
}
}
Next, you need to test when your count has reached the value in updateUIWhen
. You also need to increment updateUIWhen
once you do reach 1,000. Here is one way to do that:
-(void) bigTask{
@autoreleasepool {
int updateUIWhen = 1000;
for(int i=0;i<10000;i++){
NSString *newString = [NSString stringWithFormat:@"i = %i", i];
NSLog(@"%@", newString);
if(i == updateUIWhen){
updateUIWhen = updateUIWhen + 1000;
}
}
[self.myActivityIndicator stopAnimating];
}
}
Now you can figure out the percentage complete by dividing i
by 10,000. Put that line of code at the first spot after the opening of the if
statement.
-(void) bigTask{
@autoreleasepool {
int updateUIWhen = 1000;
for(int i=0;i<10000;i++){
NSString *newString = [NSString stringWithFormat:@"i = %i", i];
NSLog(@"%@", newString);
if(i == updateUIWhen){
float f = (float)i/10000;
updateUIWhen = updateUIWhen + 1000;
}
}
[self.myActivityIndicator stopAnimating];
}
}
If you look closely at the new line of code, you will see that i
has (float)
in front of it. This is called a type cast. In this case, you are treating i
(which is an integer) as a float
type so that you can assign the division result to a float
type. You need a float
type because your progress view is going to need a value between 0 and 1.
Next, create an NSNumber
object because that is what the method that will execute on the main thread will need as a parameter.
-(void) bigTask{
@autoreleasepool {
int updateUIWhen = 1000;
for(int i=0;i<10000;i++){
NSString *newString = [NSString stringWithFormat:@"i = %i", i];
NSLog(@"%@", newString);
if(i == updateUIWhen){
float f = (float)i/10000;
NSNumber *percentDone = [NSNumber numberWithFloat:f];
updateUIWhen = updateUIWhen + 1000;
}
}
[self.myActivityIndicator stopAnimating];
}
}
You now have everything you need to send your message to the user interface back on the main thread. To send a message to the main thread, you can use the NSObject
method performSelectorOnMainThread:withObject:waitUntilDone:
. You need a method to pass with the @selector
keyword, an object that will serve as a parameter to the method (the NSNumber
you just created) and a BOOL
indicating whether you want to block the current thread until the method has completed.
This code will send that message:
-(void) bigTask{
@autoreleasepool {
int updateUIWhen = 1000;
for(int i=0;i<10000;i++){
NSString *newString = [NSString stringWithFormat:@"i = %i", i];
NSLog(@"%@", newString);
if(i == updateUIWhen){
float f = (float)i/10000;
NSNumber *percentDone = [NSNumber numberWithFloat:f];
[self performSelectorOnMainThread:
@selector(updateProgressViewWithPercentage:)
withObject:percentDone
waitUntilDone:YES];
updateUIWhen = updateUIWhen + 1000;
}
}
[self.myActivityIndicator stopAnimating];
}
}
Here you are sending a message to the main thread where the user interface is executing with a parameter that has information about how much of the task is complete. All the code needed to update the progress view is located in the updateProgressViewWithPercentage:
method.
Next, you need to send yet another message to the main thread after the task is complete just to make sure the progress view is completely filled up when you are done. This message looks similar to what you just did.
-(void) bigTask{
@autoreleasepool {
int updateUIWhen = 1000;
for(int i=0;i<10000;i++){
NSString *newString = [NSString stringWithFormat:@"i = %i", i];
NSLog(@"%@", newString);
if(i == updateUIWhen){
float f = (float)i/10000;
NSNumber *percentDone = [NSNumber numberWithFloat:f];
[self performSelectorOnMainThread:
@selector(updateProgressViewWithPercentage:)
withObject:percentDone
waitUntilDone:YES];
updateUIWhen = updateUIWhen + 1000;
}
}
[self performSelectorOnMainThread:@selector(updateProgressViewWithPercentage:)
withObject:[NSNumber numberWithFloat:1.0]
waitUntilDone:YES];
[self.myActivityIndicator stopAnimating];
}
}
See Listings 6-5 through 6-8 for the code.
#import <UIKit/UIKit.h>
@class ViewController;
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) ViewController *viewController;
@end
#import "AppDelegate.h"
#import "ViewController.h"
@implementation AppDelegate
@synthesize window = _window;
@synthesize viewController = _viewController;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:
(NSDictionary *)launchOptions{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
self.viewController = [[ViewController alloc] initWithNibName:@"ViewController"
bundle:nil];
self.window.rootViewController = self.viewController;
[self.window makeKeyAndVisible];
return YES;
}
@end
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@property(strong) UIButton *myButton;
@property(strong) UIActivityIndicatorView *myActivityIndicator;
@property(strong) UIProgressView *myProgressView;
@end
#import "ViewController.h"
@implementation ViewController
@synthesize myButton, myActivityIndicator, myProgressView;
-(void)updateProgressViewWithPercentage:(NSNumber *)percentDone{
[self.myProgressView setProgress:[percentDone floatValue]
animated:YES];
}
-(void) bigTask{
@autoreleasepool {
int updateUIWhen = 1000;
for(int i=0;i<10000;i++){
NSString *newString = [NSString stringWithFormat:@"i = %i", i];
NSLog(@"%@", newString);
if(i == updateUIWhen){
float f = (float)i/10000;
NSNumber *percentDone = [NSNumber numberWithFloat:f];
[self performSelectorOnMainThread:
@selector(updateProgressViewWithPercentage:)
withObject:percentDone
waitUntilDone:YES];
updateUIWhen = updateUIWhen + 1000;
}
}
[self performSelectorOnMainThread:@selector(updateProgressViewWithPercentage:)
withObject:[NSNumber numberWithFloat:1.0]
waitUntilDone:YES];
[self.myActivityIndicator stopAnimating];
}
}
//do task by detaching a new thread (watch UI to see how this works)
-(void)bigTaskAction{
[self.myActivityIndicator startAnimating];
[NSThread detachNewThreadSelector:@selector(bigTask)
toTarget:self
withObject:nil];
}
- (void)viewDidLoad{
[super viewDidLoad];
//Create button
self.myButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
self.myButton.frame = CGRectMake(20, 403, 280, 37);
[self.myButton addTarget:self
action:@selector(bigTaskAction)
forControlEvents:UIControlEventTouchUpInside];
[self.myButton setTitle:@"Do Long Task"
forState:UIControlStateNormal];
[self.view addSubview:self.myButton];
//Create activity indicator
self.myActivityIndicator = [[UIActivityIndicatorView alloc] init];
self.myActivityIndicator.frame = CGRectMake(142, 211, 37, 37);
self.myActivityIndicator.activityIndicatorViewStyle =
UIActivityIndicatorViewStyleWhiteLarge;
self.myActivityIndicator.hidesWhenStopped = NO;
[self.view addSubview:self.myActivityIndicator];
//Create label
self.myProgressView = [[UIProgressView alloc] init];
self.myProgressView.frame = CGRectMake(20, 20, 280, 9);
[self.view addSubview:self.myProgressView];
}
@end
To use this code, start by setting up a Single View based application in Xcode, like you did in Recipe 1.12. Next, add the code from Listing 6-8tk to your own ViewController
class.
Run the application. In the iOS Simulator you should see an app with one button and an empty progress view. You will also see a white activity indicator in the middle of the view. Touch the button to start bigTask
in the background thread and examine the log. As bigTask
runs, you should see the progress view filling up in 10% increments until the task is complete.
Your application uses multiple threads, but at times you need to make sure two threads are not attempting to use the same block of code. Your application could cause conflicts that may result in user confusion or files being accessed too many times.
For example, try to run the application from Recipe 6.2 but touch the button a second time after the progress view starts to fill from the first task. If you look closely, the progress view will start to jump back and forth as each thread changes the progress view’s value to the current percentage for that particular thread.
Use NSLock
to make other threads wait until the thread is done processing for key blocks of code.
For this example, let’s assume that you are starting with the application from Recipe 6.2 and your app behaves as described in the Problem section. What you want to do is make sure that bigTask
only executes in one thread at a time. This will make each thread wait its turn.
The first thing you need to do is add an NSLock
object to your view controller. The following is an example of setting this up as a property:
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@property(strong) UIButton *myButton;
@property(strong) UIActivityIndicatorView *myActivityIndicator;
@property(strong) UIProgressView *myProgressView;
@property(strong) NSLock *threadLock;
@end
This property must also be included with the @synthesize statement in the view controller’s implementation.
#import "ViewController.h"
@implementation ViewController
@synthesize myButton, myActivityIndicator, myProgressView, threadLock;
@end
NOTE: It is not absolutely necessary to include objects like this as properties. You may also simply add them as a local instance in your view controller implementation. This is a design decision that you must make. I included NSLock
like this to stay as consistent as possible with the earlier recipes.
Before you use NSLock
, you need to instantiate an object and assign this to the property that you have in place. The best place to do this in a view controller is in the viewDidLoad
method.
#import "ViewController.h"
@implementation ViewController
@synthesize myButton, myActivityIndicator, myProgressView, threadLock;
- (void)viewDidLoad{
[super viewDidLoad];
//Create the NSLock object
self.threadLock = [[NSLock alloc] init];
}
@end
All the code for the view controller is not listed here, but you can see it in Listing 6-11.
Now that you have an NSLock
object, all you need to do is lock down your thread. To do this, use the lock
message to the NSLock
object at the beginning of the thread’s code and an unlock
message at the end of the thread’s code.
This code goes in your bigTask
method, which is located in the view controller’s implementation.
-(void) bigTask{
[self.threadLock lock];
@autoreleasepool {
int updateUIWhen = 1000;
for(int i=0;i<10000;i++){
NSString *newString = [NSString stringWithFormat:@"i = %i", i];
NSLog(@"%@", newString);
if(i == updateUIWhen){
float f = (float)i/10000;
NSNumber *percentDone = [NSNumber numberWithFloat:f];
[self performSelectorOnMainThread:
@selector(updateProgressViewWithPercentage:)
withObject:percentDone
waitUntilDone:YES];
updateUIWhen = updateUIWhen + 1000;
}
}
[self performSelectorOnMainThread:@selector(updateProgressViewWithPercentage:)
withObject:[NSNumber numberWithFloat:1.0]
waitUntilDone:YES];
[self.myActivityIndicator stopAnimating];
}
[self.threadLock unlock];
}
Now you can be sure that this thread will be locked until the thread is done executing. See Listings 6-9 through 6-12 for the code.
#import <UIKit/UIKit.h>
@class ViewController;
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) ViewController *viewController;
@end
#import "AppDelegate.h"
#import "ViewController.h"
@implementation AppDelegate
@synthesize window = _window;
@synthesize viewController = _viewController;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:
(NSDictionary *)launchOptions{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
self.viewController = [[ViewController alloc] initWithNibName:@"ViewController"
bundle:nil];
self.window.rootViewController = self.viewController;
[self.window makeKeyAndVisible];
return YES;
}
@end
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@property(strong) UIButton *myButton;
@property(strong) UIActivityIndicatorView *myActivityIndicator;
@property(strong) UIProgressView *myProgressView;
@property(strong) NSLock *threadLock;
@end
#import "ViewController.h"
@implementation ViewController
@synthesize myButton, myActivityIndicator, myProgressView, threadLock;
-(void)updateProgressViewWithPercentage:(NSNumber *)percentDone{
[self.myProgressView setProgress:[percentDone floatValue]
animated:YES];
}
-(void) bigTask{
[self.threadLock lock];
@autoreleasepool {
int updateUIWhen = 1000;
for(int i=0;i<10000;i++){
NSString *newString = [NSString stringWithFormat:@"i = %i", i];
NSLog(@"%@", newString);
if(i == updateUIWhen){
float f = (float)i/10000;
NSNumber *percentDone = [NSNumber numberWithFloat:f];
[self performSelectorOnMainThread:
@selector(updateProgressViewWithPercentage:)
withObject:percentDone
waitUntilDone:YES];
updateUIWhen = updateUIWhen + 1000;
}
}
[self performSelectorOnMainThread:@selector(updateProgressViewWithPercentage:)
withObject:[NSNumber numberWithFloat:1.0]
waitUntilDone:YES];
[self.myActivityIndicator stopAnimating];
}
[self.threadLock unlock];
}
//do task by detaching a new thread (watch UI to see how this works)
-(void)bigTaskAction{
[self.myActivityIndicator startAnimating];
[NSThread detachNewThreadSelector:@selector(bigTask)
toTarget:self
withObject:nil];
}
- (void)viewDidLoad{
[super viewDidLoad];
//Create the NSLock object
self.threadLock = [[NSLock alloc] init];
//Create button
self.myButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
self.myButton.frame = CGRectMake(20, 403, 280, 37);
[self.myButton addTarget:self
action:@selector(bigTaskAction)
forControlEvents:UIControlEventTouchUpInside];
[self.myButton setTitle:@"Do Long Task"
forState:UIControlStateNormal];
[self.view addSubview:self.myButton];
//Create activity indicator
self.myActivityIndicator = [[UIActivityIndicatorView alloc] init];
self.myActivityIndicator.frame = CGRectMake(142, 211, 37, 37);
self.myActivityIndicator.activityIndicatorViewStyle =
UIActivityIndicatorViewStyleWhiteLarge;
self.myActivityIndicator.hidesWhenStopped = NO;
[self.view addSubview:self.myActivityIndicator];
//Create label
self.myProgressView = [[UIProgressView alloc] init];
self.myProgressView.frame = CGRectMake(20, 20, 280, 9);
[self.view addSubview:self.myProgressView];
}
@end
To test this code, start by setting up a Single View based application in Xcode, like you did in Recipe 1.12. Next, add the code from Listing 6-12 to your own ViewController
class.
Run the application. In the iOS Simulator you should see an app with one button and an empty progress view. You will also see a white activity indicator in the middle of the view. Touch the button two times to start bigTask
running in two background threads.
As bigTask
runs, you should see the progress view filling up in 10% increments until the task is complete. Observe that the progress view fills up to 100% before going back to 0% and progressing again to 100%. NSLock
is doing what it’s supposed to do.
Your application uses multiple threads, but at times you need to make sure two threads are not attempting to use the same block of code, and you would like an alternative to NSLock
.
NOTE: @synchronized
and NSLock
solve the same problem with threads, so this recipe will look very similar to Recipe 6.3. These two approaches, while similar, are implemented differently to allow @synchronized
the ability to handle exceptions. This also causes @synchronized
to have more of a performance hit than NSLock
.
Use the application from Recipe 6.2 as a starting point. The application from Recipe 6.2 will execute the thread as many times as you touch the button, causing the progress view to behave unexpectedly.
To make sure that only one thread may use a block of code at a time, enclose the entire block of code in curly braces preceded by the @synchronized
directive.
For this example, let’s assume that you are starting with the application from Recipe 6.2 and that your app behaves as described in the Problem section. You want to make sure that bigTask
only executes in one thread at a time. This will make each thread wait its turn.
All the code for the view controller is not listed here, but you can see it in Listing 6-15.
Unlike Recipe 6.3, you will not need to add any property code. All you need to do is enclose the code in the bigTask
in curly braces with the @synchronized
directive.
-(void) bigTask{
@synchronized(self){
@autoreleasepool {
int updateUIWhen = 1000;
for(int i=0;i<10000;i++){
NSString *newString = [NSString stringWithFormat:@"i = %i", i];
NSLog(@"%@", newString);
if(i == updateUIWhen){
float f = (float)i/10000;
NSNumber *percentDone = [NSNumber numberWithFloat:f];
[self performSelectorOnMainThread:
@selector(updateProgressViewWithPercentage:)
withObject:percentDone
waitUntilDone:YES];
updateUIWhen = updateUIWhen + 1000;
}
}
[self performSelectorOnMainThread:
@selector(updateProgressViewWithPercentage:)
withObject:[NSNumber numberWithFloat:1.0]
waitUntilDone:YES];
[self.myActivityIndicator stopAnimating];
}
}
}
See Listings 6-13 through 6-16 for the code.
#import <UIKit/UIKit.h>
@class ViewController;
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) ViewController *viewController;
@end
#import "AppDelegate.h"
#import "ViewController.h"
@implementation AppDelegate
@synthesize window = _window;
@synthesize viewController = _viewController;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:
(NSDictionary *)launchOptions{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
self.viewController = [[ViewController alloc] initWithNibName:@"ViewController"
bundle:nil];
self.window.rootViewController = self.viewController;
[self.window makeKeyAndVisible];
return YES;
}
@end
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@property(strong) UIButton *myButton;
@property(strong) UIActivityIndicatorView *myActivityIndicator;
@property(strong) UIProgressView *myProgressView;
@end
#import "ViewController.h"
@implementation ViewController
@synthesize myButton, myActivityIndicator, myProgressView;
-(void)updateProgressViewWithPercentage:(NSNumber *)percentDone{
[self.myProgressView setProgress:[percentDone floatValue]
animated:YES];
}
-(void) bigTask{
@synchronized(self){
@autoreleasepool {
int updateUIWhen = 1000;
for(int i=0;i<10000;i++){
NSString *newString = [NSString stringWithFormat:@"i = %i", i];
NSLog(@"%@", newString);
if(i == updateUIWhen){
float f = (float)i/10000;
NSNumber *percentDone = [NSNumber numberWithFloat:f];
[self performSelectorOnMainThread:
@selector(updateProgressViewWithPercentage:)
withObject:percentDone
waitUntilDone:YES];
updateUIWhen = updateUIWhen + 1000;
}
}
[self performSelectorOnMainThread:
@selector(updateProgressViewWithPercentage:)
withObject:[NSNumber numberWithFloat:1.0]
waitUntilDone:YES];
[self.myActivityIndicator stopAnimating];
}
}
}
//do task by detaching a new thread (watch UI to see how this works)
-(void)bigTaskAction{
[self.myActivityIndicator startAnimating];
[NSThread detachNewThreadSelector:@selector(bigTask)
toTarget:self
withObject:nil];
}
- (void)viewDidLoad{
[super viewDidLoad];
//Create button
self.myButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
self.myButton.frame = CGRectMake(20, 403, 280, 37);
[self.myButton addTarget:self
action:@selector(bigTaskAction)
forControlEvents:UIControlEventTouchUpInside];
[self.myButton setTitle:@"Do Long Task"
forState:UIControlStateNormal];
[self.view addSubview:self.myButton];
//Create activity indicator
self.myActivityIndicator = [[UIActivityIndicatorView alloc] init];
self.myActivityIndicator.frame = CGRectMake(142, 211, 37, 37);
self.myActivityIndicator.activityIndicatorViewStyle =
UIActivityIndicatorViewStyleWhiteLarge;
self.myActivityIndicator.hidesWhenStopped = NO;
[self.view addSubview:self.myActivityIndicator];
//Create label
self.myProgressView = [[UIProgressView alloc] init];
self.myProgressView.frame = CGRectMake(20, 20, 280, 9);
[self.view addSubview:self.myProgressView];
}
@end
To test this code, start by setting up a Single View based application in Xcode, like you did in Recipe 1.12. Next, add the code from Listing 6-16 to your own ViewController
class.
Run the application. In the iOS Simulator you should see an app with one button and an empty progress view at the top of the view. You will also see a white activity indicator in the middle of the view. Touch the button two times to start bigTask
running in two background threads.
As bigTask
runs, you should see the progress view filling up in 10% increments until the task is complete. Observe that the progress view fills up to 100% before going back to 0% and progressing again to 100%. @synchronized
is doing what it’s supposed to do.
You want to implement asynchronous processing in your application, you plan on supporting your application on newer OSX and iOS systems, and you would rather not use NSThread
with the various locking mechanisms required to make your app thread safe.
NOTE: Applications that use multiple threads can become more complicated and sometimes slower because developers need to worry about situations where regions of code, resources, or data structures may be accessed by more than one thread at the same time. Keeping threads locked for short periods of time (as in Recipes 6.3 and 6.4) helps to make code thread safe (safe for use by multiple threads). However, this can prevent applications from making full use of the available resources.
Consider using Grand Central Dispatch (GCD) as an alternative to NSThread
if you know your users have updated systems (or if you choose to support updated systems only). GCD solves the same problems as NSThread
and follows the same basic idea of executing code asynchronously. GCD is a newer technology that is more efficient in computers that have multiple processors. GCD was introduced for OSX in version 10.6 and iOS in version 4.
You don’t need to do anything special to add GCD support to your application if you are developing with a version of OSX that supports GCD. GCD does require the use of a programming technique called blocks, which can take some getting used to.
Blocks are regions of code that are treated like objects. This means that you can put lines of code between curly brackets and then treat them like an object. Usually, you will see blocks used as a parameter to a method, and that is how blocks are used in GCD. You will see how blocks are used with GCD in the following section.
For this recipe, you are going to solve the same problem as you did in Recipes 6.1, 6.2, and 6.3. Essentially, you have an iOS app with a button that sets off a long task and you want to keep the user interface responsive and let the progress view fill up as the task executes. This time you will use GCD to fix this problem.
GCD uses blocks instead of methods (with the @selector
directive). This means that you don’t need to put all the code that you want to execute into a new method. Instead, you will pass the code into a GCD function as a parameter right from the action method bigTaskAction
. Use the GCD function dispatch_async
to do this.
-(void)bigTaskAction{
[self.myActivityIndicator startAnimating];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
int updateUIWhen = 1000;
for(int i=0;i<10000;i++){
NSString *newString = [NSString stringWithFormat:@"i = %i", i];
NSLog(@"%@", newString);
if(i == updateUIWhen){
float f = (float)i/10000;
NSNumber *percentDone = [NSNumber numberWithFloat:f];
updateUIWhen = updateUIWhen + 1000;
}
}
});
}
Let’s look at the first line of code with that GCD function.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
The first part is the name of the function is dispatch_async
, which is a GCD function that executes asynchronously. There is also a similar function that executes code synchronously called dispatch_sync
. The first parameter that the function requires is a dispatch queue.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
You are supplying another function that in turn returns the default dispatch queue for this application.
NOTE: GCD has a concept of code queues that are scheduled to run on the next available processor. When you use GCD you need to specify what queue that you want your code to be put into. You are using the default queue here, which you can use for background processing too. You can also use the main queue, which is like the main thread for the user interface.
The next parameter in this function is the code block. You know that you are working with a code block because it starts off with the ^
symbol and has a beginning curly bracket.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
All the lines of code that come after are part of the block parameter. This code is scheduled to execute when the next processor is available. The entire GCD function ends with the code );
.
So far, all you are doing is scheduling this big task to execute in the background. But, you still want to update the user interface as the task progresses. Instead of performing a selector on the main thread, you can use another GCD function to update the user interface on the main thread. This task should be done synchronously, so use the GCD function along with a main dispatch queue.
dispatch_sync(dispatch_get_main_queue(), ^{
[self.myProgressView setProgress:[percentDone floatValue]
animated:YES];
});
This takes the place of the method you had to code before. You can just use the variables on hand without worrying about passing parameters, as you can see when you put this GCD call into the context of the entire code block.
-(void)bigTaskAction{
[self.myActivityIndicator startAnimating];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
int updateUIWhen = 1000;
for(int i=0;i<10000;i++){
NSString *newString = [NSString stringWithFormat:@"i = %i", i];
NSLog(@"%@", newString);
if(i == updateUIWhen){
float f = (float)i/10000;
NSNumber *percentDone = [NSNumber numberWithFloat:f];
dispatch_sync(dispatch_get_main_queue(), ^{
[self.myProgressView setProgress:[percentDone floatValue]
animated:YES];
});
updateUIWhen = updateUIWhen + 1000;
}
}
});
}
NOTE: With GCD dispatch queues you don’t know for sure what order code blocks will execute when you use dispatch_async
. The system picks the most efficient way. So, if order is important (like when you are updating your interface), use dispatch_sync
.
Finally, just to be complete, you want to finish filling the progress view when the task is complete and stop the activity indicator. Use GCD again to do this by scheduling another task for the main queue at the end of the block of code.
-(void)bigTaskAction{
[self.myActivityIndicator startAnimating];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
int updateUIWhen = 1000;
for(int i=0;i<10000;i++){
NSString *newString = [NSString stringWithFormat:@"i = %i", i];
NSLog(@"%@", newString);
if(i == updateUIWhen){
float f = (float)i/10000;
NSNumber *percentDone = [NSNumber numberWithFloat:f];
dispatch_sync(dispatch_get_main_queue(), ^{
[self.myProgressView setProgress:[percentDone floatValue]
animated:YES];
});
updateUIWhen = updateUIWhen + 1000;
}
}
dispatch_sync(dispatch_get_main_queue(), ^{
[self.myProgressView setProgress:1.0
animated:YES];
[self.myActivityIndicator stopAnimating];
});
});
}
See Listings 6-17 through 6-20 for the code.
#import <UIKit/UIKit.h>
@class ViewController;
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) ViewController *viewController;
@end
#import "AppDelegate.h"
#import "ViewController.h"
@implementation AppDelegate
@synthesize window = _window;
@synthesize viewController = _viewController;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:
(NSDictionary *)launchOptions{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
self.viewController = [[ViewController alloc] initWithNibName:@"ViewController"
bundle:nil];
self.window.rootViewController = self.viewController;
[self.window makeKeyAndVisible];
return YES;
}
@end
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@property(strong) UIButton *myButton;
@property(strong) UIActivityIndicatorView *myActivityIndicator;
@property(strong) UIProgressView *myProgressView;
@end
#import "ViewController.h"
@implementation ViewController
@synthesize myButton, myActivityIndicator, myProgressView;
-(void)bigTaskAction{
[self.myActivityIndicator startAnimating];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
int updateUIWhen = 1000;
for(int i=0;i<10000;i++){
NSString *newString = [NSString stringWithFormat:@"i = %i", i];
NSLog(@"%@", newString);
if(i == updateUIWhen){
float f = (float)i/10000;
NSNumber *percentDone = [NSNumber numberWithFloat:f];
dispatch_sync(dispatch_get_main_queue(), ^{
[self.myProgressView setProgress:[percentDone floatValue]
animated:YES];
});
updateUIWhen = updateUIWhen + 1000;
}
}
dispatch_sync(dispatch_get_main_queue(), ^{
[self.myProgressView setProgress:1.0
animated:YES];
[self.myActivityIndicator stopAnimating];
});
});
}
- (void)viewDidLoad{
[super viewDidLoad];
//Create button
self.myButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
self.myButton.frame = CGRectMake(20, 403, 280, 37);
[self.myButton addTarget:self
action:@selector(bigTaskAction)
forControlEvents:UIControlEventTouchUpInside];
[self.myButton setTitle:@"Do Long Task"
forState:UIControlStateNormal];
[self.view addSubview:self.myButton];
//Create activity indicator
self.myActivityIndicator = [[UIActivityIndicatorView alloc] init];
self.myActivityIndicator.frame = CGRectMake(142, 211, 37, 37);
self.myActivityIndicator.activityIndicatorViewStyle =
UIActivityIndicatorViewStyleWhiteLarge;
self.myActivityIndicator.hidesWhenStopped = NO;
[self.view addSubview:self.myActivityIndicator];
//Create label
self.myProgressView = [[UIProgressView alloc] init];
self.myProgressView.frame = CGRectMake(20, 20, 280, 9);
[self.view addSubview:self.myProgressView];
}
@end
To test this code, start by setting up a Single View based application in Xcode, like you did in Recipe 1.12. Next, add the code from Listing 6-20tk to your own ViewController
class.
Run the application. In the iOS Simulator you should see an app with one button and an empty progress view. You will also see a white activity indicator in the middle of the view. Touch the button to start the bigTask
running. As bigTask
runs, you should see the progress view filling up in 10% increments until the task is complete.
Generally speaking, GCD is the preferred way to do background processing. If you are targeting newer systems, GCD should be your first choice when deciding what technology to implement. GCD has been optimized for multi-core applications so you will see a large boast in your application’s performance when using GCD on multi-core Macs.
GCD is simpler to use as compared to NSThread
since there is no need for an additional object nor is there a need to code an additional method as you would for NSThread
. However, you will see plenty of examples of NSThread
to do background processing, and that option is available to you.
You use GCD to perform asynchronous processing and you have a situation where you require blocks to execute one at a time in the order in which they are encountered in code. For example, in Recipe 6.5 you run into the same problem as you did earlier in this chapter when users touch the button after the long task is running (the progress view bounces back and forth).
Previously, you solved this problem using NSLock
or @synchronized()
but these come at a cost, which negates some of the benefits of using GCD in the first place.
Instead of locking the code, use a GCD serial queue to load up the code blocks to be executed in the order in which the code blocks were placed in the queue. You can use the GCD function dispatch_queue_create(DISPATCH_QUEUE_SERIAL, 0
) to create a serial queue. Make sure that the serial queue stays in scope for the lifetime of the object it serves.
For the purposes of this recipe, you’ll alter Recipe 6.5 to use a serial queue to fix the problem that you run into. The first thing you need is a property for the serial queue.
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@property(strong) UIButton *myButton;
@property(strong) UIActivityIndicatorView *myActivityIndicator;
@property(strong) UIProgressView *myProgressView;
@property(assign) dispatch_queue_t serialQueue;
@end
This could also be a local instance in the view controller, as long as the queue stays in scope as long as needed.
You need to make sure that the serial queue is implemented in the view controller’s @synthesize
statement as well.
#import "ViewController.h"
@implementation ViewController
@synthesize myButton, myActivityIndicator, myProgressView, serialQueue;
@end
The viewDidLoad
view controller method is a great place to locate the code required to create the serial queue.
#import "ViewController.h"
@implementation ViewController
@synthesize myButton, myActivityIndicator, myProgressView, serialQueue;
...
- (void)viewDidLoad{
[super viewDidLoad];
self.serialQueue = dispatch_queue_create(DISPATCH_QUEUE_SERIAL, 0);
}
...
@end
This function requires a parameter to specify the type of queue to create. Here you are using DISPATCH_QUEUE_SERIAL
because you want a serial queue that ensures that only one block of code executes at a time in the order that each block of code was placed in the queue. Some of the view controller code has been left out; see Listing 6-23 for the entire view controller code.
The next change that you need to make to the Recipe 6.5 code is to replace the default queue used before with the serial queue you just created. This happens in the bigTaskAction
method.
#import "ViewController.h"
@implementation ViewController
@synthesize myButton, myActivityIndicator, myProgressView, serialQueue;
...
-(void)bigTaskAction{
dispatch_async(self.serialQueue, ^{
dispatch_sync(dispatch_get_main_queue(), ^{
[self.myActivityIndicator startAnimating];
});
int updateUIWhen = 1000;
for(int i=0;i<10000;i++){
NSString *newString = [NSString stringWithFormat:@"i = %i", i];
NSLog(@"%@", newString);
if(i == updateUIWhen){
float f = (float)i/10000;
NSNumber *percentDone = [NSNumber numberWithFloat:f];
dispatch_sync(dispatch_get_main_queue(), ^{
[self.myProgressView setProgress:[percentDone floatValue]
animated:YES];
});
updateUIWhen = updateUIWhen + 1000;
}
}
dispatch_sync(dispatch_get_main_queue(), ^{
[self.myProgressView setProgress:1.0
animated:YES];
[self.myActivityIndicator stopAnimating];
});
});
}
...
@end
As you can see, you moved the message to start animating the activity indicator to be inside the main block for this action. You also put it into the main queue because it involved updating the user interface. The reasoning is that you want the activity indicator to start spinning each time a block like this executes in the serial queue. See Listings 6-21 through 6-24 for the code.
#import <UIKit/UIKit.h>
@class ViewController;
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) ViewController *viewController;
@end
#import "AppDelegate.h"
#import "ViewController.h"
@implementation AppDelegate
@synthesize window = _window;
@synthesize viewController = _viewController;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:
(NSDictionary *)launchOptions{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
self.viewController = [[ViewController alloc] initWithNibName:@"ViewController"
bundle:nil];
self.window.rootViewController = self.viewController;
[self.window makeKeyAndVisible];
return YES;
}
@end
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@property(strong) UIButton *myButton;
@property(strong) UIActivityIndicatorView *myActivityIndicator;
@property(strong) UIProgressView *myProgressView;
@property(assign) dispatch_queue_t serialQueue;
@end
#import "ViewController.h"
@implementation ViewController
@synthesize myButton, myActivityIndicator, myProgressView, serialQueue;
-(void)bigTaskAction{
dispatch_async(self.serialQueue, ^{
dispatch_sync(dispatch_get_main_queue(), ^{
[self.myActivityIndicator startAnimating];
});
int updateUIWhen = 1000;
for(int i=0;i<10000;i++){
NSString *newString = [NSString stringWithFormat:@"i = %i", i];
NSLog(@"%@", newString);
if(i == updateUIWhen){
float f = (float)i/10000;
NSNumber *percentDone = [NSNumber numberWithFloat:f];
dispatch_sync(dispatch_get_main_queue(), ^{
[self.myProgressView setProgress:[percentDone floatValue]
animated:YES];
});
updateUIWhen = updateUIWhen + 1000;
}
}
dispatch_sync(dispatch_get_main_queue(), ^{
[self.myProgressView setProgress:1.0
animated:YES];
[self.myActivityIndicator stopAnimating];
});
});
}
- (void)viewDidLoad{
[super viewDidLoad];
self.serialQueue = dispatch_queue_create(DISPATCH_QUEUE_SERIAL, 0);
//Create button
self.myButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
self.myButton.frame = CGRectMake(20, 403, 280, 37);
[self.myButton addTarget:self
action:@selector(bigTaskAction)
forControlEvents:UIControlEventTouchUpInside];
[self.myButton setTitle:@"Do Long Task"
forState:UIControlStateNormal];
[self.view addSubview:self.myButton];
//Create activity indicator
self.myActivityIndicator = [[UIActivityIndicatorView alloc] init];
self.myActivityIndicator.frame = CGRectMake(142, 211, 37, 37);
self.myActivityIndicator.activityIndicatorViewStyle =
UIActivityIndicatorViewStyleWhiteLarge;
self.myActivityIndicator.hidesWhenStopped = NO;
[self.view addSubview:self.myActivityIndicator];
//Create label
self.myProgressView = [[UIProgressView alloc] init];
self.myProgressView.frame = CGRectMake(20, 20, 280, 9);
[self.view addSubview:self.myProgressView];
}
@end
To test out this code, start by setting up an application like the one in Recipe 6.5.
Run the application. In the iOS Simulator you should see an app with one button and an empty progress view. You will also see a white activity indicator in the middle of the view. Touch the button two times to start bigTask
running in two background threads.
As bigTask
runs, you should see the progress view filling up in 10% increments until the task is complete. This process will repeat based on the amount of times you touch the button. The progress view should not keep jumping back and forth.
You would like to add asynchronous processing to your app, but you prefer to use a more object-oriented approach than the GCD approach.
Use NSOperationQueue
if you want to use GCD but would rather not use the GCD library directly.
NOTE: NSOperationQueue
is available for iOS 2 and above and OSX 10.5 and above. This makes NSOperationQueue
ideal when you want to support applications that run on older systems and you would rather not use NSThread
with thread locking. When you use NSOperationQueue
, the details of the implementation are hidden from you. Older systems will support NSOperationQueue
with threads while newer systems will use GCD.
NSOperationQueue
represents a queue of code that will execute. You can use NSOperationQueue
to run code in the background or in a main queue for user interface actions.
NSOperationQueue
can add code in a few ways. If the OS supports blocks (iOS 4 and above and OSX 10.6 and above), you can just add code directly to a queue using the addOperationWithBlock:
method.
If not, you must set up the code that you want to execute as a separate subclass that is a subclass of NSOperation
. A subclass of NSOperation
will act like a block in that the class will encapsulate data and code that will execute in a queue.
For this recipe, you are going to solve the same problem that was presented in Recipe 6.3. But, instead of using threads that you must lock, you will use an operation queue and the main queue to dispatch code asynchronously. Again, start with Recipe 6.2 as a template and change it to use operation queues (see Listings 6-25 through 6-28 for the complete code in context).
First, add local instances for the main queue and a serial queue right in the view controller’s implementation.
#import "ViewController.h"
@implementation ViewController
@synthesize myButton, myActivityIndicator, myProgressView;
NSOperationQueue *serialQueue;
NSOperationQueue *mainQueue;
@end
The main queue is where the user interface gets its instructions. The serial queue executes code blocks one at a time in the order in which they are received, just like the GCD serial queue in Recipe 6.6.
The viewDidLoad
method is the ideal place to instantiate these two queue objects.
#import "ViewController.h"
@implementation ViewController
@synthesize myButton, myActivityIndicator, myProgressView;
NSOperationQueue *serialQueue;
NSOperationQueue *mainQueue;
- (void)viewDidLoad{
[super viewDidLoad];
//Create the operation queues
mainQueue = [NSOperationQueue mainQueue];
serialQueue = [[NSOperationQueue alloc] init];
serialQueue.maxConcurrentOperationCount = 1;
}
@end
You can just get a reference to the main queue by using the NSOperationQueue mainQueue
method. This is a Singleton that always returns an instance of the main queue. You set up the serial queue using the alloc
and init
constructor. By setting the maxConcurrentOperationCount
to one, you are making this a serial queue because it may only do one operation at a time.
Once you have the queues set up, you can use them to schedule your blocks right from the bigTaskAction
method.
-(void)bigTaskAction{
[serialQueue addOperationWithBlock: ^{
[mainQueue addOperationWithBlock: ^{
[self.myActivityIndicator startAnimating];
}];
int updateUIWhen = 1000;
for(int i=0;i<10000;i++){
NSString *newString = [NSString stringWithFormat:@"i = %i", i];
NSLog(@"%@", newString);
if(i == updateUIWhen){
float f = (float)i/10000;
NSNumber *percentDone = [NSNumber numberWithFloat:f];
[mainQueue addOperationWithBlock: ^{
[self.myProgressView setProgress:[percentDone floatValue]
animated:YES];
}];
updateUIWhen = updateUIWhen + 1000;
}
}
[mainQueue addOperationWithBlock: ^{
[self.myProgressView setProgress:1.0
animated:YES];
[self.myActivityIndicator stopAnimating];
}];
}];
}
If you went through Recipe 6.6, you can see that this is essentially following the same pattern as what you did with GCD. See Listings 6-25 through 6-28 for the code.
#import <UIKit/UIKit.h>
@class ViewController;
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) ViewController *viewController;
@end
#import "AppDelegate.h"
#import "ViewController.h"
@implementation AppDelegate
@synthesize window = _window;
@synthesize viewController = _viewController;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:
(NSDictionary *)launchOptions{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
self.viewController = [[ViewController alloc] initWithNibName:@"ViewController"
bundle:nil];
self.window.rootViewController = self.viewController;
[self.window makeKeyAndVisible];
return YES;
}
@end
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@property(strong) UIButton *myButton;
@property(strong) UIActivityIndicatorView *myActivityIndicator;
@property(strong) UIProgressView *myProgressView;
@end
#import "ViewController.h"
@implementation ViewController
@synthesize myButton, myActivityIndicator, myProgressView;
NSOperationQueue *serialQueue;
NSOperationQueue *mainQueue;
-(void)bigTaskAction{
[serialQueue addOperationWithBlock: ^{
[mainQueue addOperationWithBlock: ^{
[self.myActivityIndicator startAnimating];
}];
int updateUIWhen = 1000;
for(int i=0;i<10000;i++){
NSString *newString = [NSString stringWithFormat:@"i = %i", i];
NSLog(@"%@", newString);
if(i == updateUIWhen){
float f = (float)i/10000;
NSNumber *percentDone = [NSNumber numberWithFloat:f];
[mainQueue addOperationWithBlock: ^{
[self.myProgressView setProgress:[percentDone floatValue]
animated:YES];
}];
updateUIWhen = updateUIWhen + 1000;
}
}
[mainQueue addOperationWithBlock: ^{
[self.myProgressView setProgress:1.0
animated:YES];
[self.myActivityIndicator stopAnimating];
}];
}];
}
- (void)viewDidLoad{
[super viewDidLoad];
//Create the operation queues
mainQueue = [NSOperationQueue mainQueue];
serialQueue = [[NSOperationQueue alloc] init];
serialQueue.maxConcurrentOperationCount = 1;
//Create button
self.myButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
self.myButton.frame = CGRectMake(20, 403, 280, 37);
[self.myButton addTarget:self
action:@selector(bigTaskAction)
forControlEvents:UIControlEventTouchUpInside];
[self.myButton setTitle:@"Do Long Task"
forState:UIControlStateNormal];
[self.view addSubview:self.myButton];
//Create activity indicator
self.myActivityIndicator = [[UIActivityIndicatorView alloc] init];
self.myActivityIndicator.frame = CGRectMake(142, 211, 37, 37);
self.myActivityIndicator.activityIndicatorViewStyle =
UIActivityIndicatorViewStyleWhiteLarge;
self.myActivityIndicator.hidesWhenStopped = NO;
[self.view addSubview:self.myActivityIndicator];
//Create label
self.myProgressView = [[UIProgressView alloc] init];
self.myProgressView.frame = CGRectMake(20, 20, 280, 9);
[self.view addSubview:self.myProgressView];
}
@end
Run the application. In the iOS Simulator you should see an app with one button and an empty progress view. You will also see a white activity indicator in the middle of the view. Touch the button two times to start bigTask
running in two background threads.
As bigTask
runs, you should see the progress view filling up in 10% increments until the task is complete. This process will repeat based on the number of times you touched the button. The progress view should not keep jumping back and forth.
18.219.228.88