Make a Clock In Sprite Kit: Adding a Button for a Stopwatch

iOS Simulator Screen shot May 13, 2014, 7.45.19 AMIn the clock app so far, we have a date and a time. We can switch between them by tapping the screen of our phone. Most apps use a button for this, and it’s time we added our first button. Our app will now change to a stopwatch mode when we press the button.

Re-position the Elements on the Grid

We will need room for the stop watch button at vertical grid position 2, where the date hides now.  Begin by changing a few grid positions. In initWithSize: change theses lines

myDateLabel.position = [self gridPointX:2.5 pointY:3.0];
...
CGPoint tick = [self gridPointX:0.5 pointY:1];
CGPoint tock = [self gridPointX:4.5 pointY:1];

In touchesBegan: change this:

CGPoint bottomDisplay = [self gridPointX:2.5 pointY:3.0];

Make a Sprite Button

Now back in initWithSize: add the following code to make a button:

//make a switch to a stopwatch
SKSpriteNode *modeButton= [SKSpriteNode spriteNodeWithImageNamed:@"solidcircle"];
modeButton.name = @"modeButton";
modeButton.position = [self gridPointX:4.0 pointY:2];
[self addChild:modeButton];

Detect a Tap of the Button

Re-arrange touchesBegan: and make a few changes:

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

    CGPoint topDisplay = [self gridPointX:2.5 pointY:4];
    CGPoint bottomDisplay = [self gridPointX:2.5 pointY:3.0];

    SKAction *labelHideAction = [SKAction group:@[
                                                  [SKAction fadeAlphaTo:0.2 duration:3.0],
                                                  [SKAction moveToY:bottomDisplay.y duration:3.0]
                                                  ]];
    SKAction *labelShowAction = [SKAction group:@[
                                                  [SKAction fadeInWithDuration:3],
                                                  [SKAction moveToY:topDisplay.y duration:3.0]]];

    for (UITouch *touch in touches) {
        [myTimeLabel removeActionForKey:@"timeLabelAction"];
        [myDateLabel removeActionForKey:@"dateLabelAction"];
        //toggle action
            if(isShowingTime){
                [myTimeLabel runAction:labelHideAction withKey:@"timeLabelAction"];
                [myDateLabel runAction:labelShowAction withKey:@"dateLabelAction"];
            }else{
                [myTimeLabel runAction:labelShowAction withKey:@"timeLabelAction"];
                [myDateLabel runAction:labelHideAction withKey:@"dateLabelAction"];
            }
            isShowingTime = !isShowingTime;

    }
}

We made one significant change. The method touchesBegan: has a NSSet for a parameter. The code changed from checking if it has any elements with [touches count]>0 to enumerating the touches that are elements in the set. As touchesBegan: loops through the touches, there is a specific touch position to check. The function CGRectContainsPoint (CGRect rect,CGPoint point) returns a BOOL which tells us if a point is in a specific rectangle. In our case, we will make that rect the frame for the button.  Add this code just under the for loop:

[myTimeLabel removeActionForKey:@"timeLabelAction"];
[myDateLabel removeActionForKey:@"dateLabelAction"];

CGRect modeButtonRect=[[self childNodeWithName:@"modeButton"] frame];
if (CGRectContainsPoint(modeButtonRect, [touch locationInNode:self])){
//code for stopwatch here

// using the date display of the stopwatch, display the date on top
    if(isShowingTime){
        [myTimeLabel runAction:labelHideAction withKey:@"timeLabelAction"];
        [myDateLabel runAction:labelShowAction withKey:@"dateLabelAction"];
    }
        isStopWatch = YES;
        isStartingStopwatch = YES;
        isShowingTime = NO;

}else{
// shut down Stopwatch if on
            isStopWatch = NO;

I’ve mentioned in earlier posts it is a good idea to name your nodes. Here is an example why. We use the method childNodeWithName: to get the frame of the button. The if statement compares the touch to this frame. If there is contact within this frame, the if executes code for the button. If I was being more faithful to MVC, I’d probably call a method here, but for demonstration purposes I’ll put the handler inside the if block. For any other kind of touch, we execute our current code for toggling between the clock and the date, plus changing the flag to NO for the stopwatch, so it disappears on such a touch.

When we use a stopwatch, we will display it in the date label. The code moves the date label to the top, then sets some flags that we are starting the stopwatch.

To get this code to work, add one more closing brace after isShowingTime = !isShowingTime; to close the else block. Add the following instance variables:

BOOL isStopWatch;
BOOL isStartingStopwatch;
CFTimeInterval startTime;

Make the Stopwatch

In update: find the code for the date formatting:

    NSString *formattedDateString =[NSString stringWithFormat:@"%04d %02d %02d", (int)currentDate.year, currentDate.month, currentDate.day];
    myDateLabel.text = formattedDateString;

Add to that code the following:

    if(isStopWatch){
        //start the stopwatch by getting a point for elapsed time
        if(isStartingStopwatch){
            startTime=currentTime;
            isStartingStopwatch=NO;
        }
        //update the time in the stopwatch
        CFGregorianDate elapsedTime  = CFAbsoluteTimeGetGregorianDate((currentTime - startTime), nil);
        NSString *formattedDateString = [NSString stringWithFormat:@"%02d:%02d:%02.1f", elapsedTime.hour, elapsedTime.minute, elapsedTime.second];
        myDateLabel.text = formattedDateString;
    }else{
        NSString *formattedDateString =[NSString stringWithFormat:@"%04d %02d %02d", (int)currentDate.year, currentDate.month, currentDate.day];
    myDateLabel.text = formattedDateString;
    }

When isStopWatch is YES, we check if this is the first frame we have run the stopwatch. If so, we also grab currentTime as our starting time. We subtract the starting time from the current time to get an elapsed time. The code formats the time to a string. We do not need a time zone for a stopwatch, so the time zone is nil.

Test the Stop Watch

In initWithSize: initalize isStopWatch with NO. Build and run:

iOS Simulator Screen shot May 13, 2014, 7.44.53 AM

You now have a stationary dot. Press the dot and the stopwatch appears:

iOS Simulator Screen shot May 13, 2014, 7.45.19 AM

Tap anywhere on the scene and the date re-appears with the time floating up to the top:

iOS Simulator Screen shot May 13, 2014, 7.45.04 AM

Stopwatches generally have more than one button. Next time, we will add some more buttons for start, stop and reset, then animate their appearance.

The Code: What We Did Today

//
//  MPMyScene.m
//  SpriteTimeClock
//
//  Created by Steven Lipton on 5/6/14.
//  Copyright (c) 2014 Steven Lipton. All rights reserved.
//

#import "MPMyScene.h"

@implementation MPMyScene{
    SKLabelNode *myTimeLabel;
    SKLabelNode *myDateLabel;
    BOOL isShowingTime;
    BOOL isStopWatch;
    BOOL isStartingStopwatch;
    CFTimeInterval startTime;
}

-(CGPoint)gridPointX:(float)xPoint pointY:(float)yPoint{
    CGFloat xDivision = CGRectGetMaxX(self.frame) /5.0;
    CGFloat yDivision = CGRectGetMaxY(self.frame)/5.0;
    return CGPointMake(xPoint * xDivision, yPoint * yDivision);
}

-(id)initWithSize:(CGSize)size {
    if (self = [super initWithSize:size]) {
        /* Setup your scene here */
        myTimeLabel = [SKLabelNode labelNodeWithFontNamed:@"HelveticaNeue"];
        myTimeLabel.text = @"00:00:00";
        myTimeLabel.fontSize = 40;
        myTimeLabel.position = CGPointMake(CGRectGetMidX(self.frame),
                                       CGRectGetMaxY(self.frame)*0.80);
        myTimeLabel.name = @"myTimeLabel";

         [self addChild:myTimeLabel];

        self.backgroundColor = [SKColor colorWithRed:0.15 green:0.15 blue:0.3 alpha:1.0];

        myDateLabel = [SKLabelNode labelNodeWithFontNamed:@"HelveticaNeue"];
        myDateLabel.text = @"Date Goes Here";
        myDateLabel.fontSize = 40;
        myDateLabel.alpha = 0.20;
        myDateLabel.position = [self gridPointX:2.5 pointY:3.0];
        myDateLabel.name = @"myDateLabel";

        [self addChild:myDateLabel];

        //make a switch to a stopwatch
        SKSpriteNode *modeButton= [SKSpriteNode spriteNodeWithImageNamed:@"solidcircle"];
        modeButton.name = @"modeButton";
        modeButton.position = [self gridPointX:4.0 pointY:2];
        [self addChild:modeButton];

        //Make the ball
        CGPoint tick = [self gridPointX:0.5 pointY:1];
        CGPoint tock = [self gridPointX:4.5 pointY:1];
        SKSpriteNode *bouncingBall = [SKSpriteNode spriteNodeWithImageNamed:@"solidcircle"];
        bouncingBall.position = tock;
        bouncingBall.name=@"bouncingBall";
        [self addChild:bouncingBall];

/*
        //simple animation to the ball
        SKAction *bounceBall = [SKAction moveToX:tick.x duration:1.0];
        [bouncingBall runAction:bounceBall withKey:@"bounceBall"];

*/
        //add animation to the ball
        SKAction *bounceBall = [SKAction sequence:@[
                                                    [SKAction moveToX:tock.x duration:1.0],
                                                    [SKAction moveToX:tick.x duration:1.0],
                                                    ]];
        bounceBall.timingMode = SKActionTimingEaseInEaseOut;
        [bouncingBall runAction:[SKAction repeatActionForever:bounceBall] withKey:@"bounceBall"];

        isShowingTime = YES;
        isStopWatch = NO;
    }

    return self;
}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

    CGPoint topDisplay = [self gridPointX:2.5 pointY:4];
    CGPoint bottomDisplay = [self gridPointX:2.5 pointY:3.0];

    SKAction *labelHideAction = [SKAction group:@[
                                                  [SKAction fadeAlphaTo:0.2 duration:3.0],
                                                  [SKAction moveToY:bottomDisplay.y duration:3.0]
                                                  ]];
    SKAction *labelShowAction = [SKAction group:@[
                                                  [SKAction fadeInWithDuration:3],
                                                  [SKAction moveToY:topDisplay.y duration:3.0]]];

    for (UITouch *touch in touches) {
        [myTimeLabel removeActionForKey:@"timeLabelAction"];
        [myDateLabel removeActionForKey:@"dateLabelAction"];

        CGRect modeButtonRect=[[self childNodeWithName:@"modeButton"] frame];
        if (CGRectContainsPoint(modeButtonRect, [touch locationInNode:self])){
            //code for stopwatch here

            //shrink the display a bit

            //display the date on top
            if(isShowingTime){
                [myTimeLabel runAction:labelHideAction withKey:@"timeLabelAction"];
                [myDateLabel runAction:labelShowAction withKey:@"dateLabelAction"];
            }
            isStopWatch = YES;
            isStartingStopwatch = YES;
            isShowingTime = NO;

        }else{
            // shut down Stopwatch if on
            isStopWatch = NO;
            //toggle action
            if(isShowingTime){
                [myTimeLabel runAction:labelHideAction withKey:@"timeLabelAction"];
                [myDateLabel runAction:labelShowAction withKey:@"dateLabelAction"];
            }else{
                [myTimeLabel runAction:labelShowAction withKey:@"timeLabelAction"];
                [myDateLabel runAction:labelHideAction withKey:@"dateLabelAction"];
            }
            isShowingTime = !isShowingTime;
        }
    }
}

-(void)update:(CFTimeInterval)currentTime {

    // time display
    /* Called before each frame is rendered */
    CFGregorianDate currentDate = CFAbsoluteTimeGetGregorianDate(CFAbsoluteTimeGetCurrent(), CFTimeZoneCopySystem());
    CFRelease(CFTimeZoneCopySystem());
    NSString *formattedTimeString = [NSString stringWithFormat:@"%02d:%02d:%02.0f", currentDate.hour, currentDate.minute, currentDate.second];
    myTimeLabel.text = formattedTimeString;

    //date or stopwatch display

    if(isStopWatch){
        //start the stopwatch by getting a point for elapsed time
        if(isStartingStopwatch){
            startTime=currentTime;
            isStartingStopwatch=NO;
        }
        //update the time in the stopwatch
        CFGregorianDate elapsedTime  = CFAbsoluteTimeGetGregorianDate((currentTime - startTime), nil);
        NSString *formattedDateString = [NSString stringWithFormat:@"%02d:%02d:%02.1f", elapsedTime.hour, elapsedTime.minute, elapsedTime.second];
        myDateLabel.text = formattedDateString;
    }else{
        NSString *formattedDateString =[NSString stringWithFormat:@"%04d %02d %02d", (int)currentDate.year, currentDate.month, currentDate.day];
    myDateLabel.text = formattedDateString;
    }

}

@end

2 thoughts on “Make a Clock In Sprite Kit: Adding a Button for a Stopwatch”

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s