Getting Started - Part 3: Adding a Stopwatch with NSTimer and Our First Class

Getting Started Series by Nick Schneider

Welcome back for Part 3! Today we are going to implement the stopwatch.  We are going to start off right where we left off. If you haven't worked through it yet, you can either head over to Part 1 and Part 2 or grab the Project Here for today's starting point.

Motivation

One of the key ingredients in calculating our reading rate is the time our user spent reading. It would be annoying to make them use another app or device to keep track of the time for them, so we want to make our own stopwatch right on the calculator for them to use.

Open Xcode and Turn Off Autolayout

Load your project and open up the Main.storyboard file. The first thing we want to do is check is to make sure Autolayout is off. Open up the right utility view by clicking the right panel button above View.  Click on the File Inspector, and then make sure Autolayout is unchecked. We will spend some time on Autolayout later on in the series. For now, we are ready to add our new view elements.

Add New View Elements

Drag on a new view to hold the stopwatch elements. Resize it to fill about a third of the full view just below our Words Per Minute label. Open up the Attributes Inspector to change the Background to the Light Gray Color option. This is to help distinguish the view from the rest of the calculator. Next, grab three Buttons and name them Start, Stop, and Reset.

Now we will place one more new view as if it were the screen of the stopwatch. Inside that view place a Label that says 00:00:00, and is centered with bold, size 28 font. When the two new views, three buttons, and the label have all been placed, go ahead and build and run your project. You should see something that looks like the following:

iOS Simulator Screen shot Nov 13, 2013, 6.40.56 PM.png

Connecting To Code

Open up the Assistant Editor so you can connect your new elements to code. First, connect the start button by holding Control and left-click and drag over to the ViewController.h file. You want to select Action as the Connection type, use the defaults, and give it the name startButtonPressed . Do the same for the stop and reset buttons and name them stopButtonPressed and resetButtonPressed, respectively. Lastly, connect the time label to the ViewController.h using an outlet, and call it stopwatchTimeLabel. In the end, the following lines should now appear above the @end:

- (IBAction)startButtonPressed:(id)sender;
- (IBAction)stopButtonPressed:(id)sender;
- (IBAction)resetButtonPressed:(id)sender;

@property (weak, nonatomic) IBOutlet UILabel *stopwatchTimeLabel;

Implementing our Stopwatch with NSTimer

Now we are ready to go ahead with the implementation of the stopwatch. We will utilize an NSTimer object to do the counting for us. In addition we will want to keep track of the how much time has elapsed, and since our reading times will usually be in minutes, we do not need to keep track beyond seconds. In the ViewController.h file, add the following just before @end

@property (weak, nonatomic) NSTimer *myTimer;
@property int currentTimeInSeconds;

It would also be good to add some comments to your file to help with organization and readability. You can use "//" To start a singe line comment. Go ahead and add a comment above your stopwatch UI elements by adding "// Stopwatch Components". You will also want to go add a line about the calculator components by adding "// Calculator Components" near the top of the file.

Next, we will create our NSTimer object. Open up ViewController.m, and add #import "Foundation/Foundation.h" at the top of the file below #import "ViewController.h". Now write a function to handle creating our timer object and one to handle the timer "ticking" by adding the following code just above @end:

 

- (NSTimer *)createTimer {
return [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(timerTicked:)
userInfo:nil
repeats:YES];
}

- (void)timerTicked:(NSTimer *)timer {

_currentTimeInSeconds++;

}

Here our createTimer function initializes an NSTimer object that fires once ever 1.0 seconds repeatedly. Every time it fires, it sends a message to the timerTicked function, which then increments the _currentTimeInSeconds property. Note that we prepend the property with an underscore. This is the variable automatically created by Xcode when we declare a property. This saves us from having to initialize the property ourselves. Less code to write for you!

Whenever we call the createTimer functions, we will keep track of the time in seconds. Now we need to convert the total number of seconds to how many hours, minutes, and seconds that have elapsed so we can properly display on our stop watch. Go ahead and write one more function that returns our time as a formatted string. It should look like:

- (NSString *)formattedTime:(int)totalSeconds
{

int seconds = totalSeconds % 60;
int minutes = (totalSeconds / 60) % 60;
int hours = totalSeconds / 3600;

return [NSString stringWithFormat:@"%02d:%02d:%02d",hours, minutes, seconds];
}

Where we have taken our integer total number of seconds and broken it up into its three parts. The number of seconds is the remained after dividing by 60. For the minutes, we first divide by 60, and then take the remained after a second division by 60. Lastly, the number of hours is simply the integer part of the total divided by 3600. The least step is to return the time as formatted string to give hh:mm:ss  as would be expected on a digital clock.

Lastly, we need to update the stopwatchTimeLabel the user sees with this formatted time. We modify the timerTicked method to include the updating. Set self.stopwatchTimerLabel.text using a call to the formattedTime method. This will give:

- (void)timerTicked:(NSTimer *)timer {

_currentTimeInSeconds++;

self.stopwatchTimeLabel.text = [self formattedTime:_currentTimeInSeconds];

}

Adding Functionality to Start, Stop, and Reset Buttons

Now we need to make it so start, stop, and reset actually perform actions. When the user presses the start button, what should happen? If the stopwatch had not started, it should start no matter what time it reads, and if the stopwatch is running, nothing should happen. We can accomplish this by modifying the startButtonPressed function that Xcode generated to read:

- (IBAction)startButtonPressed:(id)sender {

if (!_currentTimeInSeconds) {
_currentTimeInSeconds = 0 ;
}

if (!_myTimer) {
_myTimer = [self createTimer];
}

}

In this function we first make sure our currentTimeInSeconds variable exists, and we set it to 0 if not. Next, we check to see if our timer object exists, and if it does not, we create one by calling the createTimer function we just wrote.

For the stop button, we simply want the stopwatch to stop counting. We can achieve this by calling the invalidate method on our timer object. Modify your stopButtonPressed function to call this method.

- (IBAction)stopButtonPressed:(id)sender {

[_myTimer invalidate];

}

Lastly, we need to consider how the reset button should act. Obviously we want the time to be set to 0, but what should happen when the stopwatch is and is not running? When the stopwatch is not running, we want it to stay that way, but if the user hits reset while it is running, we just want to set the currentTimeInSeconds to 0 and start over. Write the following for the resetButtonPressed function:

- (IBAction)resetButtonPressed:(id)sender {

if (_myTimer) {
[_myTimer invalidate];
_myTimer = [self createTimer];
}

_currentTimeInSeconds = 0;

self.stopwatchTimeLabel.text = [self formattedTime:_currentTimeInSeconds];

}

We will need to invalidate the timer and recreate it to make sure the new timer is counting correctly. If we did not do this, hitting reset in the middle of the timer count would lead to the first second displaying more quickly than after one full second. This is just to help the user experience and ensure accuracy.

Build and run your project to see you have a working stopwatch! 

 

 

iOS Simulator Screen shot Nov 13, 2013, 7.02.17 PM.png

A Little Extra - Challenge Edition!

There are a few more features we can add to make for a better user experience. This time, however, I want you to try and figure them out on your own. We have already gone over enough that you should be able to work your way through these two tasks.

Challenge 1 - Autofill Time In Minutes

Add functionality so that when you hit the stop button, timeInMinutesTextField is automatically updated. Format the number to be in minutes and have two decimal places.

Challenge 2 - Time Label Formatting

Most of the time when reading and testing your reading rate, you read for a few minutes or some time less than a full hour. Make it so the stopwatchTimeLabel has the format of mm:ss until the timer has been running for an hour, where the format changes to the current format.

Challenge 3 - Track Time when app is Closed

If you were to hit the home button on the simulator while the timer is running, you will notice that when you come back to the app, the time continues where you left off. This means you are not timing the absolute time, just the time the app is running and active. Find a way to track the full time. Hint: You may want to consider using the NSDate class.

You have a Working Stopwatch! What's Next?

Thank you for following our walk-through! We hope you found it helpful.

In Part 4 we will show the solutions to today's challenges. After that, we will continue with our Read Rates app!

Download the Project from Today

Share with us your struggles, successes, and stories below! 

 

Nicholas M Schneider

Nicholas M Schneider is a 2010 graduate from the Kate Gleason College of Engineering who is now a Doctoral Candidate at the University of Pennsylvania. Originally from an obscure town south of Buffalo, New York, he attended the Rochester Institute of Technology where he received concurrent Bachelor of Science and Master of Science degrees. While there he had a number of Co-ops including a six month stay as a Design Engineer at Lockheed Martin and Research positions with Dr. Satish Kandlikar. Nicholas currently works with Dr. Haim H Bau in the field dubbed “in situ electron microscopy of liquid systems” where he studies applications in energy and biological systems. Outside of the lab, Nicholas Schneider is a Graduate Associate in Rodin College House and enjoys running (he ran his second Philly Marathon this past November), cooking, baking, reading, and justifying his coffee addiction by making it a hobby.