A Simple Stopwatch for iPhone

An NSTimer object can be configured to fire periodically at a set time interval, and also to call a method each time it fires. In this blog, we’ll see how to use NSTimer to control a simple stopwatch interface. So let’s get started!

Start Xcode, choose “Create a new Xcode project,” then select the Single View Application template. Name the project “Stopwatch,” and choose options as shown:

Click Next, choose a location to save the project, and Click Create.

We’ll start with the ViewController.h file. Open this file, and make these changes:

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

@property (nonatomic, weak) IBOutlet UILabel *hr;
@property (nonatomic, weak) IBOutlet UILabel *min;
@property (nonatomic, weak) IBOutlet UILabel *sec;
@property (nonatomic, weak) IBOutlet UILabel *hun;

@property (nonatomic, weak) IBOutlet UIButton *btnStop;
@property (nonatomic, weak) IBOutlet UIButton *btnStart;
@property (nonatomic, weak) IBOutlet UIButton *btnReset;

@property (nonatomic, strong) NSTimer *timer;

(IBAction)startTouched:(UIButton *)sender;
(IBAction)stopTouched:(UIButton *)sender;
(IBAction)resetTouched:(UIButton *)sender;

@end

Our interface will have four labels named hr, min, sec, and hun, corresponding to hours, minutes, seconds and hundredths of a second. We make properties for these labels as outlets. There will also be three buttons: one to stop the timer, one to start it, and one to reset all the labels to zero. We make outlet properties for these three buttons as well, since we will want to be able to disable the buttons under certain conditions.

Finally, three action methods are declared to control the timer and display. These will be wired to three button controls in the interface itself. Save your work at this time.

Open ViewController.xib. Drag controls to the view as shown here:

Note that there are actually seven labels: the three labels between the ones containing “0” contain the colon (:) character. The three colon labels are not wired to the ViewController. From left to right, wire the other four labels to the hr, min, sec, and hun outlets.

Wire the Stop button to stopTouched:, the Start button to startTouched:, and the Reset Button to resetTouched:. Now save the ViewController.xib file.

Open ViewController.m. Make these changes:

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

@synthesize hr, min, sec, hun, timer;
@synthesize btnStop, btnStart, btnReset;

(IBAction)startTouched:(UIButton *)sender
{
    self.timer = [NSTimer scheduledTimerWithTimeInterval:0.01
                                                  target:self
                                                selector:@selector(showTime)
                                                userInfo:nil
                                                 repeats:YES];
   
    [self buttonStatesWithStartState:NO stopState:YES resetState:NO];
}

(IBAction)stopTouched:(UIButton *)sender
{
    [self.timer invalidate];
    self.timer = nil;
   
    [self buttonStatesWithStartState:YES stopState:NO resetState:YES];
}

(IBAction)resetTouched:(UIButton *)sender
{
    self.hr.text = @"0";
    self.min.text = @"0";
    self.sec.text = @"0";
    self.hun.text = @"0";
   
    [self buttonStatesWithStartState:YES stopState:NO resetState:NO];
}

(void)buttonStatesWithStartState:(BOOL)startState
                         stopState:(BOOL)stopState
                        resetState:(BOOL)resetState
{
    self.btnStart.enabled = startState;
    self.btnStop.enabled = stopState;
    self.btnReset.enabled = resetState;
   
    self.btnStart.alpha = self.btnStop.alpha = self.btnReset.alpha = .5;
   
    if (startState) self.btnStart.alpha = 1;
    if (stopState) self.btnStop.alpha = 1;
    if (resetState) self.btnReset.alpha = 1;
}

(void)showTime
{
    int hours = 0;
    int minutes = 0;
    int seconds = 0;
    int hundredths = 0;
    NSArray *timeArray = [NSArray arrayWithObjects:self.hun.text, self.sec.text, self.min.text, self.hr.text, nil];
   
    for (int i = [timeArray count] 1; i >= 0; i) {
        int timeComponent = [[timeArray objectAtIndex:i] intValue];
        switch (i) {
            case 3:
                hours = timeComponent;
                break;
            case 2:
                minutes = timeComponent;
                break;
            case 1:
                seconds = timeComponent;
                break;
            case 0:
                hundredths = timeComponent;
                hundredths++;
                break;
               
            default:
                break;
        }
       
    }
    if (hundredths == 100) {
        seconds++;
        hundredths = 0;
    }
    else if (seconds == 60) {
        minutes++;
        seconds = 0;
    }
    else if (minutes == 60) {
        hours++;
        minutes = 0;
    }
    self.hr.text = [NSString stringWithFormat:@"%.0d", hours];
    self.min.text = [NSString stringWithFormat:@"%.2d", minutes];
    self.sec.text = [NSString stringWithFormat:@"%.2d", seconds];
    self.hun.text = [NSString stringWithFormat:@"%.2d", hundredths];
   
}

(void)viewDidLoad
{
    [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
}

(void)viewDidUnload
{
    [super viewDidUnload];
    // Release any retained subviews of the main view.
}

(BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
}

@end

Let’s look first at the buttonStatesWithStartState: stopState: resetState: method. This method takes three BOOL parameters and sets the enabled and alpha properties of the buttons accordingly. Rather than setting these values each time they should change, it makes sense to factor code out into a private method of ViewController.

The startTouched: method first gets a new (autoreleasing) NSTimer object using the scheduledTimerWithTimeInterval: target: selector: userInfo: repeats: method. The time interval is specified in seconds: 0.01 sets this timer to fire at 1/100 second intervals. The target is self, because this class contains the selector (method) that will be called each time the timer fires. Selector is the method to fire (showTime); userInfo is nil (if we had arguments to pass to showTime, we could specify them here. A value of YES passed in the repeats argument tells the timer object to repeat indefinitely. After this object is assigned to self.timer, we turn the Start button off, the Stop button on, and the Reset Button off.

In the stopTouched method, the self.timer object is first invalidated, then set to nil. This has the effect of decoupling self.timer from any object retained on the heap. ARC will (at some point) release the memory associated with the timer because the reference count to that object is now 0. This allows us to create a new timer the next time the Start button is touched without worrying about leaking memory due to multiple strong references to timer objects. We then call buttonStatesWithStartState: YES stopState: NO resetState: YES, which turns on the Start button, turns off the Stop button, and turns on the Reset button.

The resetTouched: method simply resets all the labels to “0” and sets the Start, Stop, and Reset buttons to on, off, and off, respectively.

Each time the timer fires, the showTime method is called. We set up an NSArray containing the texts in the four labels. Then, starting with the hr labels’ text, we get the actual integer value of each of the label texts in turn. In the zero case (the hundredths label text intValue), we bump the hundredth second interval.

Next, we inspect each integer value to see if it should roll over into the next higher field. If hundredths == 100, the seconds field should be incremented, and the hundredths field is set back to 0. We do this for hundredths, seconds, and minutes. Hours can be allowed to increase without limit.

Finally, we format these int values back into the label texts. The format specifier %.2d forces an integer to be displayed with a minimum of two digits, using zeros to left-pad the value as needed. This specifier is used for minutes, seconds, and hundredths. We use %.0d for hours because no padding is desired in this label.

Here is a sample run of the application:

After Stop is touched, either Reset or Start can be pressed. Reset will set the time back to all zeros, and Start will continue counting from the time currently shown in the labels.

Leave a Reply

Your email address will not be published. Required fields are marked *