Working with the NSOperationQueue Class
Multi-tasking prevents apps from freezing. In most programming languages, achieving this is a bit tricky, but the NSOperationQueue class in iOS makes it easy!
This tutorial will demonstrate how to use the NSOperationQueue class. An NSOperationQueue object is a queue that handles objects of the NSOperationclass type. An NSOperation object, simply phrased, represents a single task, including both the data and the code related to the task. The NSOperationQueue handles and manages the execution of all the NSOperation objects (the tasks) that have been added to it. The execution takes place with the main thread of the application. When an NSOperation object is added to the queue it is executed immediately and it does not leave the queue until it is finished. A task can be cancelled, but it is not removed from the queue until it is finished. The NSOperation class is an abstract one so it cannot be used directly in the program. Instead, there are two provided subclasses, the NSInvocationOperation class and theNSBlockOperation class. I'll use the first one in this tutorial.
The Sample Project
Here's the goal for this tutorial: for each extra thread we want our application to create an NSInvocationOperation (NSOperation) object. We'll add each object into the NSOperationQueue and then we're finished. The queue takes charge of everything and the app works without freezing. To demonstrate clearly the use of the classes I mentioned above, we will create a (simple) sample project in which, apart from the main thread of the app, we will have two more threads running along with it. On the first thread, a loop will run from 1 to 10,000,000 and every 100 steps a label will be updated with the loop's counter value. On the second thread, a label's background will fill with a custom color. That process will take place inside a loop and it will be executed more than once. So we will have something like a color rotator. At the same time, the RGB values of the custom background color along with the loop counter's value will be displayed next to the label. Finally, we will use three buttons to change the view's background color on the main thread. These tasks could not be executed simultaneously without multi-tasking. Here is a look at the end result:
Step 1: Create the Project
Let's begin by creating the project. Open the Xcode and create a new Single View Application.
Click on Next and set a name for the project. I named it ThreadingTestApp. You can use the same or any other name you like.
Next. complete the project creation.
Step 2: Setup the Interface
Click on the ViewController.xib
file to reveal the Interface Builder. Add the following controls to create an interface like the next image:
- UINavigationBar
- Frame (x, y, W, H): 0, 0, 320, 44
- Tintcolor: Black color
- Title: "Simple Multi-Threading Demo"
- UILabel
- Frame (x, y, W, H): 20, 59, 280, 21
- Text: "Counter at Thread #1"
- UILabel
- Frame (x, y, W, H): 20, 88, 280, 50
- Background color: Light gray color
- Text color: Dark gray color
- Text: -
- UILabel
- Frame (x, y, W, H): 20, 154, 280, 21
- Text: "Random Color Rotator at Thread #2"
- UILabel
- Frame (x, y, W, H): 20, 183, 100, 80
- Background color: Light gray color
- Text: -
- UILabel
- Frame (x, y, W, H): 128, 183, 150, 80
- Text: -
- UILabel
- Frame (x, y, W, H): 20, 374, 280, 21
- Text: "Background Color at Main Thread"
- UIButton
- Frame (x, y, W, H): 20, 403, 73, 37
- Title: "Color #1"
- UIButton
- Frame (x, y, W, H): 124, 403, 73, 37
- Title: "Color #2"
- UIButton
- Frame (x, y, W, H): 228, 403, 73, 37
- Title: "Color #3"
For the last UILabel and the three UIButtons, set the Autosizing value to Left - Bottom to make the interface look nice on the iPhone 4/4S and iPhone 5, just like the next image:
Step 3: IBOutlet Properties and IBAction Methods
In this next step we will create the IBOutlet properties and IBAction methods that are necessary to make our sample app work. To create new properties and methods, and connect them to your controls while being the Interface Builder, click on the middle button of the Editor button at the Xcode toolbar to reveal the Assistant Editor:
Not every control needs an outlet property. We will add only one for the UILabels 3, 5, and 6 (according to the order they were listed in step 2), named label1, label2, and label3.
To insert a new outlet property, Control+Click (Right click) on a label > Click on the New Referencing Outlet > Drag and Drop into the Assistant Editor. After that, specify a name for the new property, just like in the following images:
Inserting a new IBOutlet property
Setting the IBOutlet property name
Repeat the process above three times to connect the three UILabels to properties. Inside your ViewController.h
file you have these properties declared:
1
2
3
|
@property ( retain , nonatomic ) IBOutlet UILabel *label 1 ; @property ( retain , nonatomic ) IBOutlet UILabel *label 2 ; @property ( retain , nonatomic ) IBOutlet UILabel *label 3 ; |
Now add the IBAction methods for the three UIButtons. Each one button will change the background color of the view. To insert a new IBAction method, Control+Click (Right click) on a UIButton > Click on the Touch Up Inside > Drag and Drop into the Assistant Editor. After that specify a name for the new method. Take a look at the following images and the next snippet for the method names:
Inserting a new IBAction method
Setting the IBAction method name
Again, repeat the process above three times to connect every UIButton to an action method. The ViewController.h
file should now contain these:
1
2
3
|
- ( IBAction )applyBackgroundColor 1 ; - ( IBAction )applyBackgroundColor 2 ; - ( IBAction )applyBackgroundColor 3 ; |
The IBOutlet properties and IBAction methods are now ready. We can now begin coding.
Step 4: The NSOperationQueue Object and the Necessary Task-Related Method Declarations
One of the most important tasks we must do is to declare a NSOperationQueue
object (our operation queue), which will be used to execute our tasks in secondary threads. Open the ViewController.h
file and add the following content right after the @interface
header (don't forget the curly brackets):
1
2
3
|
@interface ViewController : UIViewController{ NSOperationQueue *operationQueue; } |
Also, each task needs to have at least one method which contains the code that will run simultaneously with the main thread. According to the introductory description, the first task the method will be named counterTask
and the second one will be named colorRotatorTask
:
1
2
|
-( void )counterTask; -( void )colorRotatorTask; |
That's all we need. Our ViewController.h
file should look like this:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
@interface ViewController : UIViewController{ NSOperationQueue *operationQueue; } @property ( retain , nonatomic ) IBOutlet UILabel *label 1 ; @property ( retain , nonatomic ) IBOutlet UILabel *label 2 ; @property ( retain , nonatomic ) IBOutlet UILabel *label 3 ; - ( IBAction )applyBackgroundColor 1 ; - ( IBAction )applyBackgroundColor 2 ; - ( IBAction )applyBackgroundColor 3 ; -( void )counterTask; -( void )colorRotatorTask; @end |
Let's move on to implementation.
Step 5: Implementation
We're almost finished. We have setup our interface, made all the necessary connections, declared any needed IBAction and other methods, and established our base. Now it is time to build upon them.
Open the ViewController.m
file and go to the viewDidLoad
method. The most important part of this tutorial is going to take place here. We will create a newNSOperationQueue
instance and two NSOperation (NSInvocationOperation)
objects. These objects will encapsulate the code of the two methods we previously declared and then they will be executed on their own by the NSOperationQueue
. Here is the code:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
- ( void )viewDidLoad { [ super viewDidLoad ]; // Create a new NSOperationQueue instance. operationQueue = [ NSOperationQueue new ]; // Create a new NSOperation object using the NSInvocationOperation subclass. // Tell it to run the counterTask method. NSInvocationOperation *operation = [[ NSInvocationOperation alloc ] initWithTarget : self selector : @selector (counterTask) object :nil ]; // Add the operation to the queue and let it to be executed. [operationQueue addOperation :operation]; [operation release ]; // The same story as above, just tell here to execute the colorRotatorTask method. operation = [[ NSInvocationOperation alloc ] initWithTarget : self selector : @selector (colorRotatorTask) object :nil ]; [operationQueue addOperation :operation]; [operation release ]; } |
This whole process is really simple. After creating the NSOperationQueue
instance, we create an NSInvocationOperation object (operation). We set its selector method (the code we want executed on a separate thread), and then we add it to the queue. Once it enters the queue it immediately begins to run. After that the operation object can be released, since the queue is responsible for handling it from now on. In this case we create another object and we'll use it the same way for the second task (colorRotatorTask).
Our next task is to implement the two selector methods. Let's begin by writing thecounterTask
method. It will contain a for
loop that will run for a large number of iterations and every 100 steps the label1
's text will be updated with the current iteration's counter value (i
). The code is simple, so here is everything:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
-( void )counterTask{ // Make a BIG loop and every 100 steps let it update the label1 UILabel with the counter's value. for ( int i= 0 ; i< 1 0 0 0 0 0 0 0 ; i++) { if (i % 1 0 0 == 0 ) { // Notice that we use the performSelectorOnMainThread method here instead of setting the label's value directly. // We do that to let the main thread to take care of showing the text on the label // and to avoid display problems due to the loop speed. [label 1 performSelectorOnMainThread : @selector (setText:) withObject :[ NSString stringWithFormat : @"%d" , i ] waitUntilDone : YES ]; } } // When the loop gets finished then just display a message. [label 1 performSelectorOnMainThread : @selector (setText:) withObject : @"Thread #1 has finished." waitUntilDone : NO ]; } |
Please note that it is recommended as the best practice (even by Apple) to perform any visual updates on the interface using the main thread and not by doing it directly from a secondary thread. Therefore, the use of the performSelectorOnMainThread
method is necessary in cases such as this one.
Now let's implement the colorRotatorTask
method:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
-( void )colorRotatorTask{ // We need a custom color to work with. UIColor *customColor; // Run a loop with 500 iterations. for ( int i= 0 ; i< 5 0 0 ; i++) { // Create three float random numbers with values from 0.0 to 1.0. float redColorValue = (arc 4 random() % 1 0 0 ) * 1 .0 / 1 0 0 ; float greenColorValue = (arc 4 random() % 1 0 0 ) * 1 .0 / 1 0 0 ; float blueColorValue = (arc 4 random() % 1 0 0 ) * 1 .0 / 1 0 0 ; // Create our custom color. Keep the alpha value to 1.0. customColor = [ UIColor colorWithRed :redColorValue green :greenColorValue blue :blueColorValue alpha : 1 .0 ]; // Change the label2 UILabel's background color. [label 2 performSelectorOnMainThread : @selector (setBackgroundColor:) withObject :customColor waitUntilDone : YES ]; // Set the r, g, b and iteration number values on label3. [label 3 performSelectorOnMainThread : @selector (setText:) withObject :[ NSString stringWithFormat : @"Red: %.2f\nGreen: %.2f\nBlue: %.2f\Iteration #: %d" , redColorValue, greenColorValue, blueColorValue, i ] waitUntilDone : YES ]; // Put the thread to sleep for a while to let us see the color rotation easily. [ NSThread sleepForTimeInterval : 0 .4 ]; } // Show a message when the loop is over. [label 3 performSelectorOnMainThread : @selector (setText:) withObject : @"Thread #2 has finished." waitUntilDone : NO ]; } |
You can see that we used the performSelectorOnMainThread
method here as well. The next step is the [NSThread sleepForTimeInterval:0.4];
command, which is used to cause some brief delay (0.4 seconds) in each loop execution. Even though it is not necessary to use this method, it is preferable to use it here to slow down the background color's changing speed of the label2
UILabel (our color rotator). Additionally in each loop we create random values for the red, green, and blue. We then set these values to produce a custom color and set it as a background color in the label2
UILabel.
At this point the two tasks that are going to be executed at the same time with the main thread are ready. Let's implement the three (really easy) IBAction methods and then we are ready to go. As I have already mentioned, the three UIButtons will change the view's background color, with the ultimate goal to demonstrate how the main thread can run alongside the other two tasks. Here they are:
01
02
03
04
05
06
07
08
09
10
11
|
- ( IBAction )applyBackgroundColor 1 { [ self .view setBackgroundColor :[ UIColor colorWithRed : 2 5 5 .0 / 2 5 5 .0 green : 2 0 4 .0 / 2 5 5 .0 blue : 1 0 2 .0 / 2 5 5 .0 alpha : 1 .0 ]]; } - ( IBAction )applyBackgroundColor 2 { [ self .view setBackgroundColor :[ UIColor colorWithRed : 2 0 4 .0 / 2 5 5 .0 green : 2 5 5 .0 / 2 5 5 .0 blue : 1 0 2 .0 / 2 5 5 .0 alpha : 1 .0 ]]; } - ( IBAction )applyBackgroundColor 3 { [ self .view setBackgroundColor :[ UIColor whiteColor ]]; } |
That's it! Now you can run the application and see how three different tasks can take place at the same time. Remember that when the execution of NSOperation objects is over, it will automatically leave the queue.
Conclusion
Many of you may have already discovered that the actual code to run a multi-tasking app only requires a few lines of code. It seems that the greatest workload is implementing the required methods that work with each task. Nevertheless, this method is an easy way to develop multi-threading apps in iOS.