In iPhone / iPad development, executing code on a thread other than the main UI thread is usually done to keep a long running process from blocking or stalling the user interface. In iOS, there are a couple of ways to perform tasks on new threads. In this blog, we take a look at the simplest of these, NSThread.
We’ll begin with an example of what happens when a long running process executes on the main thread, then move the process to a new thread of execution and note the difference. Start Xcode and create a new single view iPhone application named SimpleThread. Create this simple user interface, consisting of a single button and label:
Edit ViewController.h and add a property for the label and an action method header for the button:
#import <UIKit/UIKit.h> @interface ViewController : UIViewController @property (nonatomic, weak) IBOutlet UILabel *output; - (IBAction)goPressed:(UIButton *)sender; @end
Wire up the label outlet and button method. Make sure the button method is wired to the TouchUpInside event of the button.
Open ViewController.m and add the methods shown:
@implementation ViewController - (IBAction)goPressed:(UIButton *)sender { [self longloop]; } - (void)longloop { for (long i = 0; YES; i++) { //change the text in the label on the main thread: self.output.text = [NSString stringWithFormat:@"%ld", i]; sleep(1); } } //...
If we run the app now, we’ll notice two interesting things. First, the label text doesn’t update as expected, and second, the button becomes inactive. The user interface has been blocked by the longloop method, which tries to update the label’s text to the content of the loop counter (i) once per second.
Even though it is unlikely that such code would occur in a production app, longloop is a naive example of what could occur if an app were running any long running process, such as looking up information on the web, etc. Blocking the user interface is always a bad idea: it leads to user frustration and bad feelings about an app and its developer. What we need here is a thread…
Luckily, in this case, we can get away with the simplest type of threading: NSThread. To use NSThread, we must create an instance of it, then set some of its properties. Reopen ViewController.h and add a property for the thread:
#import <UIKit/UIKit.h> @interface ViewController : UIViewController @property (nonatomic, weak) IBOutlet UILabel *output; @property (nonatomic, strong) NSThread *thread; - (IBAction)goPressed:(UIButton *)sender; @end
We’ll also make some changes to ViewController.m (the full file is shown):
#import "ViewController.h" @interface ViewController () @end @implementation ViewController - (NSThread *) thread { if (!_thread) { _thread = [[NSThread alloc] initWithTarget:self selector:@selector(longloop) object:nil]; } return _thread; } - (IBAction)goPressed:(UIButton *)sender { if ([self.thread isExecuting]) { NSLog(@"Stopping thread"); [self.thread cancel]; } else { NSLog(@"Starting thread"); [self.thread start]; } } - (void)longloop { for (long i = 0; YES; i++) { //change the text in the label on the main thread: [self performSelector:@selector(updateOutput:) onThread:[NSThread mainThread] withObject:[NSNumber numberWithLong:i] waitUntilDone:NO]; sleep(1); if ([self.thread isCancelled]) { //stop the thread: self.thread = nil; break; } } } - (void) updateOutput:(NSNumber *)count { self.output.text = [NSString stringWithFormat:@"%ld", [count longValue]]; } - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated. } @end
We’ve lazily instantiated the thread property by overriding its getter. A thread must know what code to run, we give it a target (self) and a selector, here, longloop. We’ve also made some changes to goPressed and longloop as well. goPressed now checks to see if the thread is executing. If it is, it attempts to cancel the thread, if not it starts it. Simply telling a thread to cancel itself is not enough though. Cancel simply sets a flag, it doesn’t actually do anything. We need to inspect the value of the flag inside the thread code to actually cancel the thread.
Since longloop is now running on a new thread, it cannot make changes to UIControls running on the main thread. It must call a selector on the main thread which makes the change. This is now performed by the updateOutput:(NSNumber *)count method. Note the way longloop calls this method as a selector on the main thread:
[self performSelector:@selector(updateOutput:) onThread:[NSThread mainThread] withObject:[NSNumber numberWithLong:i] waitUntilDone:NO];
mainThread is always available inside an NSThread.
longloop then inspects the value of the cancel flag. If the thread has been told to cancel, we stop the running thread, which is a simple as setting it to nil. When the button is pressed again, the whole process starts over.
Run the app again and note that the label updates and the button is never inactive. This is a much better experience for the user!
The source code for this project may be found here: SimpleThread