Make Animated Stopwatch Buttons in Sprite Kit

stopwatch
Throu building a clock app, we’re exploring the uses of animation through Sprite kit for user interfaces instead of games. Last time we added the code to make a stopwatch. This time we will add three more buttons to control the stopwatch, and give them an animated presentation when the user presses the stopwatch button.

Add the Code for Making Buttons

Up to this point in the series we made sprites individually. Since we are making very similar sprites, a method for a button creation can be helpful:

-(SKSpriteNode *)makeButtonWithTitleAndName:(NSString *)title withImageNamed:(NSString *)imageName{
    SKSpriteNode *aSprite = [SKSpriteNode spriteNodeWithImageNamed:imageName];
    aSprite.name = title;
    [aSprite setScale:0.75];
    SKLabelNode *titleNode = [SKLabelNode labelNodeWithFontNamed:@"HelveticaNeue"];
    titleNode.text = title;
    titleNode.name = @"title"; //Will let us modify the title at runtime
    titleNode.fontSize = 16;
    titleNode.verticalAlignmentMode = SKLabelVerticalAlignmentModeCenter;
    titleNode.position = CGPointMake(CGRectGetMidX(aSprite.frame), CGRectGetMidY(aSprite.frame));
    titleNode.fontColor = [UIColor blackColor];
    [aSprite addChild:titleNode];
    return aSprite;

}

Line 2 and 3 of this code is nothing new — it creates a sprite. Line 4 changes the scale of the dot to three-quarters its original size. Line 5-12 is something we have yet to do. We set up a label node and then add it to the sprite. It now becomes a child of the sprite, and what we do to the sprite, we do to the label. Sometimes, we may need to change the label and not the sprite. In line 7, we assign the name title to the label, and we can use the childNodewithName: method on the sprite to change properties of the label when we need to make a change.
Positioning labels is a bit difficult due to the verticalAlignmentMode property. By default, the vertical alignment mode is set to align to the baseline of the text. When centering a label on a sprite for a button, this will place the label too high on the button background. Line 9 changes the vertical alignment to SKLabelVerticalAlignmentModeCenter which works a lot better for centering in the button.

Make a Method to Place the Buttons

Now that we have a method to make buttons, we can make some. Add the following code below what we just added:

-(void)makeStopWatchButtonsAtGridPoint:(CGPoint)gridPoint{
    NSString *image = @"solidcircle";
    //make button 1 -- start
    SKSpriteNode *startButton = [self makeButtonWithTitleAndName:@"Start" withImageNamed:image];
    startButton.position = [self gridPointX:gridPoint.x pointY:gridPoint.y];
    [self addChild:startButton];
    //make button 2 -- stop
    SKSpriteNode *stopButton = [self makeButtonWithTitleAndName:@"Stop" withImageNamed:image];
    stopButton.position = [self gridPointX:gridPoint.x pointY:gridPoint.y];
    [self addChild:stopButton];
    //make button 3 -- reset
    SKSpriteNode *resetButton = [self makeButtonWithTitleAndName:@"Reset" withImageNamed:image];
    resetButton.position = [self gridPointX:gridPoint.x pointY:gridPoint.y];
    [self addChild:resetButton];
    //make a switch to a stopwatch
    SKSpriteNode *modeButton= [self makeButtonWithTitleAndName:@"Mode" withImageNamed:image];
    modeButton.position = [self gridPointX:gridPoint.x pointY:gridPoint.y];
    modeButton.scale = 1.0;
    [self addChild:modeButton];

}

Much of this code is a repetition. Had there been more buttons, a loop would have been a better way to code this. We get a button from our method above with different names, then place it at a specific grid point. Note that the last button we add is the Mode button, what was our original stopwatch on/off button. Sprite Kit adds sprites one on top of the other. The last node will be the visible top node. Since we want our mode button on top, we add that one last. We also want that to be full size, so the method changes the scale back to 1.0.

We can now add this to our code. Change the previous mode button code in initWithSize: as follows:

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

        //add the stopwatch buttons
        [self makeStopWatchButtonsAtGridPoint:CGPointMake(4.0, 2.0)];
 

Make an Animation Method for the Button

Our next step is to make the animation  method for the buttons. Just like creating the buttons, the work is a bit repetitive. We first create a method to move a sprite horizontally on our layout grid. If the flag is yes we will show the sprite, if the flag is no, we will hide it.

(SKAction *)moveToGridPositionX:(CGFloat)gridPointX fadeIn:(BOOL)isFadein duration:(NSTimeInterval)duration{
    CGPoint point = [self gridPointX:gridPointX pointY:0.0];
    SKAction *move =[SKAction moveToX:point.x duration:duration];
    SKAction *fadeInOut;
    if (isFadein){
        fadeInOut = [SKAction fadeInWithDuration:duration ];
    }else{
        fadeInOut = [SKAction fadeOutWithDuration:duration];
    }
    return [SKAction group:@[move,fadeInOut]];
}

Make a Method to Animate the Buttons

We now add our last method, which executes the animation. Again, we use a Boolean flag to either show or hide our button display. The buttons start hidden under the Mode button, but then spread out tot he left, while the mode button moves over to the side to be a semi-circle, and fade out its text.

-(void)animatedStopwatchMenuOpen:(BOOL)isOpen{
    //a method to make buttons appear for the stopwatch
    NSTimeInterval duration = 1.0;
    SKAction *moveButton;
    //get our buttons
    SKNode *modeButton =[self childNodeWithName:@"Mode"];
    SKNode *startButton = [self childNodeWithName:@"Start"];
    SKNode *stopButton = [self childNodeWithName:@"Stop"];
    SKNode *resetButton = [self childNodeWithName:@"Reset"];
    SKNode *title;
    if (isOpen){
    //push the on off to the side
        moveButton = [self moveToGridPositionX:5 fadeIn:YES duration:duration];
        [modeButton runAction:moveButton];
        title = [modeButton childNodeWithName:@"title"];
        [title runAction:[SKAction fadeOutWithDuration:duration]];
    //start to position 1
        moveButton = [self moveToGridPositionX:1 fadeIn:YES duration:duration];
        [startButton runAction:moveButton];
    //stop to position 2
        moveButton = [self moveToGridPositionX:2 fadeIn:YES duration:duration];
        [stopButton runAction:moveButton];
    //reset to position 3
        moveButton = [self moveToGridPositionX:3 fadeIn:YES duration:duration];
        [resetButton runAction:moveButton];
    }else{
    //close the menu
        moveButton = [self moveToGridPositionX:4 fadeIn:NO duration:duration];
        title = [modeButton childNodeWithName:@"title"];
        [title runAction:[SKAction fadeInWithDuration:duration]];
        [startButton runAction:moveButton];
        [stopButton runAction:moveButton];
        [resetButton runAction:moveButton];
        moveButton = [self moveToGridPositionX:4 fadeIn:YES duration:duration];
        [modeButton runAction:moveButton];
    }
}

As discussed earlier, line 15 gives us access to the title of the button, and in this case we fade it out in line 16. We could have changed the title if we wanted to, but that requires a bit more work. The method childNodewithName: returns a SKNode. We would have to cast, and possibly type check, the SKNode to a SKLabelNode in order to access the text property.

The rest of this code moves sprite to the correct position. I could have easily used a loop here too, but for this example, I didn’t.

Hook it Up to the Clock App

In touchesBegan: change the code to add the new animation method:

if (CGRectContainsPoint(modeButtonRect, [touch locationInNode:self])){
            //code for stopwatch here

            //show the stopwatch buttons
            [self animatedStopwatchMenuOpen:YES];
 

and also here:

}else{
            // shut down Stopwatch if on
            isStopWatch = NO;
            //move buttons back
            [self animatedStopwatchMenuOpen:NO];

The first set of code animates the buttons to open when the user presses the Mode button. When the user taps anywhere else, the buttons disappear.

stopwatch

In our next installment we will add some actions to our buttons, and start and stop the stopwatch.

 

The Whole Code

Here’s the code for the scene in one nice file.

//
//  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;
    BOOL isRunningStopwatch;
    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.0];
        //[self addChild:modeButton];
        
        //add the stopwatch buttons
        [self makeStopWatchButtonsAtGridPoint:CGPointMake(4.0, 2.0)];
        
        
        //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 setScale:0.75];
        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;
}

-(SKSpriteNode *)makeButtonWithTitleAndName:(NSString *)title withImageNamed:(NSString *)imageName{
    SKSpriteNode *aSprite = [SKSpriteNode spriteNodeWithImageNamed:imageName];
    aSprite.name = title;
    [aSprite setScale:0.75];
    SKLabelNode *titleNode = [SKLabelNode labelNodeWithFontNamed:@"HelveticaNeue"];
    titleNode.text = title;
    titleNode.name = @"title";
    titleNode.fontSize = 16;
    titleNode.verticalAlignmentMode = SKLabelVerticalAlignmentModeCenter;
    titleNode.position = CGPointMake(CGRectGetMidX(aSprite.frame), CGRectGetMidY(aSprite.frame));
    titleNode.fontColor = [UIColor blackColor];
    [aSprite addChild:titleNode];
    return aSprite;
    
}

-(void)makeStopWatchButtonsAtGridPoint:(CGPoint)gridPoint{
    NSString *image = @"solidcircle";
    //make a base layeranchor is on the right side center.
    //anchor is right middle
        //make button 1 -- start
    SKSpriteNode *startButton = [self makeButtonWithTitleAndName:@"Start" withImageNamed:image];
    startButton.position = [self gridPointX:gridPoint.x pointY:gridPoint.y];
    [self addChild:startButton];
    //make button 2 -- stop
    SKSpriteNode *stopButton = [self makeButtonWithTitleAndName:@"Stop" withImageNamed:image];
    stopButton.position = [self gridPointX:gridPoint.x pointY:gridPoint.y];
    [self addChild:stopButton];
    //make button 3 -- reset
    SKSpriteNode *resetButton = [self makeButtonWithTitleAndName:@"Reset" withImageNamed:image];
    resetButton.position = [self gridPointX:gridPoint.x pointY:gridPoint.y];
    [self addChild:resetButton];
    
    //make a switch to a stopwatch
    SKSpriteNode *modeButton= [self makeButtonWithTitleAndName:@"Mode" withImageNamed:image];
    modeButton.position = [self gridPointX:gridPoint.x pointY:gridPoint.y];
    modeButton.scale = 1.0;
    [self addChild:modeButton];

}

-(SKAction *)moveToGridPositionX:(CGFloat)gridPointX fadeIn:(BOOL)isFadein duration:(NSTimeInterval)duration{
    CGPoint point = [self gridPointX:gridPointX pointY:0.0];
    SKAction *move =[SKAction moveToX:point.x duration:duration];
    SKAction *fadeInOut;
    if (isFadein){
        fadeInOut = [SKAction fadeInWithDuration:duration ];
    }else{
        fadeInOut = [SKAction fadeOutWithDuration:duration];
    }
    return [SKAction group:@[move,fadeInOut]];
}


-(void)animatedStopwatchMenuOpen:(BOOL)isOpen{
    //a method to make buttons appear for the stopwatch
    NSTimeInterval duration = 1.0;
    SKAction *moveButton;
    //get our buttons
    SKNode *modeButton =[self childNodeWithName:@"Mode"];
    SKNode *startButton = [self childNodeWithName:@"Start"];
    SKNode *stopButton = [self childNodeWithName:@"Stop"];
    SKNode *resetButton = [self childNodeWithName:@"Reset"];
    SKNode *title;
    //open the menu -- do last
    //fade in the menu
    //scale the menu out  to the edge
    
    if (isOpen){
    //push the on off to the side
        moveButton = [self moveToGridPositionX:5 fadeIn:YES duration:duration];
        [modeButton runAction:moveButton];
        title = [modeButton childNodeWithName:@"title"];
        [title runAction:[SKAction fadeOutWithDuration:duration]];
    //start to poistion 1
        moveButton = [self moveToGridPositionX:1 fadeIn:YES duration:duration];
        [startButton runAction:moveButton];
    //stop to position 2
        moveButton = [self moveToGridPositionX:2 fadeIn:YES duration:duration];
        [stopButton runAction:moveButton];
    //reset to position 3
        moveButton = [self moveToGridPositionX:3 fadeIn:YES duration:duration];
        [resetButton runAction:moveButton];
    }else{
    //close the menu
        moveButton = [self moveToGridPositionX:4 fadeIn:NO duration:duration];
        title = [modeButton childNodeWithName:@"title"];
        [title runAction:[SKAction fadeInWithDuration:duration]];
        
        [startButton runAction:moveButton];
        [stopButton runAction:moveButton];
        [resetButton runAction:moveButton];
        moveButton = [self moveToGridPositionX:4 fadeIn:YES duration:duration];
        [modeButton runAction:moveButton];
    }
}


-(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"];
        SKNode *modeButton =[self childNodeWithName:@"Mode"];
        CGRect modeButtonRect=modeButton.frame;
        if (CGRectContainsPoint(modeButtonRect, [touch locationInNode:self])&& !isStopWatch){
            //code for stopwatch here
            
            //show the stopwatch buttons
            [self animatedStopwatchMenuOpen:YES];
            
            //display the date on top
            if(isShowingTime){
                [myTimeLabel runAction:labelHideAction withKey:@"timeLabelAction"];
                [myDateLabel runAction:labelShowAction withKey:@"dateLabelAction"];
            }
            isStopWatch = YES;
            isStartingStopwatch = YES;
            isShowingTime = NO;
            isRunningStopwatch = YES;
        }else{
            // shut down Stopwatch if on
            isStopWatch = NO;
            //move buttons back
            [self animatedStopwatchMenuOpen:NO];
            //move the button to the side
            //CGPoint point =[self gridPointX:4.0 pointY:2.0];
            //[modeButton runAction:[SKAction moveToX:point.x duration:1.0 ]];
            //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;
        }
        if (isRunningStopwatch) {
            
        //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

 

One thought on “Make Animated Stopwatch Buttons in Sprite Kit”

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