In 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:
You now have a stationary dot. Press the dot and the stopwatch appears:
Tap anywhere on the scene and the date re-appears with the time floating up to the top:
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
Leave a Reply