How to properly calculate 1 second with deltaTime in Swift - swift

I'm trying to calculate an elapsed second in deltaTime but I'm not sure how to do it, because my deltaTime constantly prints 0.0166 or 0.0167.
Here is my code:
override func update(_ currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
deltaTime = currentTime - lastTime
lastTime = currentTime
How do I make it so I can squeeze some logic in here to run every second?
EDIT: I was able to come up with the following, but is there a better way?
deltaTimeTemp += deltaTime
if (deltaTimeTemp >= 1.0) {
print(deltaTimeTemp)
deltaTimeTemp = 0
}

I always use SKActions for this type of thing: (written in swift 3)
let wait = SKAction.wait(forDuration: 1.0)
let spawnSomething = SKAction.run {
//code to spawn whatever you need
}
let repeatSpawnAction = SKAction.repeatForever(SKAction.sequence([wait, spawnSomething]))
self.run(repeatSpawnAction)

If you really only care about a 1 second interval then you should not be storing the delta for every frame. Just store the start time and calculate the elapsed time each frame. When the elapsed time exceeds your 1 second interval then you reset the start time to now.
override func update(_ currentTime: CFTimeInterval) {
let elpasedTimeSinceLastupdate = currentTime - startTime
//Check if enough time has elapsed otherwise there is nothing to do
guard elpasedTimeSinceLastupdate > requiredTimeIntervale else {
return
}
startTime = currentTime
// Do stuff here
}
I ideally you want more than 1 timer, so then you would need to maintain an array of timers and a table of intervals and blocks to call. This starts to get very complicated and really you should probably just use the built in block Timer in iOS 10, which is much more straight forward:
_ = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
//Do stuff every second here
}

currentTime: number = 0;
update(delta) {
this.time += delta / 1000;
GameCurrentData.time = Math.floor(this.time);
if(this.currentTime !== GameCurrentData.time){
Globals.emitter?.Call?.("perSecond");
this.currentTime = GameCurrentData.time;
}
}

Related

Create a stopwatch in Swift using .timeIntervalSince()

I need to create a simple stopwatch in Swift using the method timeIntervalSince().
I don't really understand how to use timeIntervalSince (what I need and how to implement it) and how to transform it into a String that will show me the passed time like "00:00:00".
I know I need to use a Timer to update the Label and invalidate it when clicking on "Stop".
I'd really appreciate any help on this. Let me know if you need more information.
The method timeIntervalSince(_:) is a method of Date. It gives you the number of seconds that have passes since some other date and the date you are asking.
So,
Create a StopwatchVC.
Give StopwatchVC a startTime var of type Date.
Also give it a Timer var. Lets call that updateTimer.
When the user taps the start button, save Date() (the time right now) into startTime. Also start a repeating timer, updateTimer that fires every 1/10 second. (or however often you want to update your stopwatch, but note that faster than 1/60 is pointless because the screen can't update any faster than that, and timers are only accurate to about 1/50 sec anyway.)
Each time updateTimer fires, calculate the number of seconds that have elapsed since the start time and display it to the screen:
let seconds = Date().timeIntervalSince(startTime)
Date() is the current date and time, with sub-millisecond accuracy.
Date().timeIntervalSince(startTime) will give you the number of seconds since startTime, again with sub-millisecond accuracy.
Format and display the elapsed time to the screen. You could use a DateComponentsFormatter or build a time string yourself using a NumberFormatter, or even String(format:)
//
// StopWatchVC.swift
// Gem
//
// Created by Macbook 5 on 4/18/22.
//
import UIKit
class StopWatchVC:UIViewController {
var timer:Timer?
var startTime = Date()
let titleLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(titleLabel)
titleLabel.frame = CGRect(x: 0, y: 0, width: 200, height: 60)
titleLabel.center = view.center
titleLabel.textColor = .red
view.backgroundColor = .white
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: (#selector(updateTimer)), userInfo: nil, repeats: true)
}
#objc func updateTimer() {
let timeInterval = Date().timeIntervalSince(startTime)
titleLabel.text = timeInterval.stringFromTimeInterval()
}
}
extension TimeInterval{
func stringFromTimeInterval() -> String {
let time = NSInteger(self)
let ms = Int((self.truncatingRemainder(dividingBy: 1)) * 1000)
let seconds = time % 60
let minutes = (time / 60) % 60
let hours = (time / 3600)
return String(format: "%0.2d:%0.2d:%0.2d.%0.3d",hours,minutes,seconds,ms)
}
}

swift call function multiple times inside update

I create a game via SpriteKit. in the game every few second a ball spawn to the screen (via function) and the player has to blow them up. now, I want to check the player level via his score so if the score is bigger than 10 the spawnBall function will be executed twice (so 2 ball will spawn on the screen) an so on. I tried to to it via the update fun (that will "read" the player score and depends on the score will call the spawnBall function). Unfortunately when I do it the screen is spawn with million (or so) balls in few seconds (as I said I want it to call the function every few seconds and increase the call while the score is X). I really don't have any idea how to do it.
here is my code:
override func update(_ currentTime: CFTimeInterval) {
if (self.score <= 10){
spawnBalls()
}
if (self.score > 10 && self.score <= 20){
spawnBalls()
spawnBalls()
}
if (self.score > 20){
spawnBalls()
spawnBalls()
spawnBalls()
}
if (self.subscore == 3) {
_ = randomBallColorToBlow()
self.subscore = 0
}
}
func spawnBalls() {
let wait = SKAction.wait(forDuration: 1)
let action = SKAction.run {
self.createBall()
}
run(SKAction.repeatForever((SKAction.sequence([wait, action]))))
}
how can I do it without using the update function??
you are calling spawn balls 60 times a second by calling it in your update func.
try just checking if a certain requirement is met to upgrade to a higher spawn rate in your update but keep the calls out of the update func.
private var upgradedToLevel2 = false
private var upgradedToLevel3 = false
//called somewhere probably in a start game func
spawnBalls(duration: 1.0)
override func update(_ currentTime: CFTimeInterval) {
if (self.score > 10 && self.score <= 20) && !upgradedToLevel2 {
//this prevents the if loop from running more than once
upgradedToLevel2 = true
self.removeAction(forKey: "spawn")
spawnBalls(duration: 0.5)
}
if (self.score > 20) && !upgradedToLevel3 {
//this prevents the if loop from running more than once
upgradedToLevel3 = true
spawnBalls(duration: 0.33)
}
}
func spawnBalls(duration: Double) {
let wait = SKAction.wait(forDuration: duration)
let action = SKAction.run { self.createBall() }
let repeater = SKAction.repeatForever(SKAction.sequence([wait, action]))
run(repeater, withKey: "spawn")
}
As stated, you are spawning your balls multiple times and need to break it up. I would recommend keeping track of level using an Int instead of a bool to be able to handle an "infinite" amount of level ups without making an "infinite" amount of boolean variables
private var nextLevel = 0 //Level 0 allows us to spawn a ball on startup, so no need to call spawnBalls anywhere else
override func update(_ currentTime: CFTimeInterval) {
if (self.score > 10 * nextLevel){
self.removeAction(forKey: "spawn") //this prevents the if loop from running more than once
nextLevel += 1
spawnBalls(count:nextLevel,forDuration:1) //You can change the 1 here if you want to spawn balls at a faster speed, I would recommend a variable that uses nextLevel in a forumula
}
}
func spawnBalls(count:Int, forDuration duration:TimeInterval) {
let range = 0..<count
let wait = SKAction.wait(forDuration: duration)
let action = SKAction.run {range.forEach{self.createBall()}}
let repeater = SKAction.repeatForever(SKAction.sequence([wait, action]))
removeAction(forKey:"spawn")
run(repeater, withKey: "spawn")
}

Smooth animation with timer and loop in iOS app

I have ViewController with stars rating that looks like this (except that there are 10 stars)
When user opens ViewController for some object that have no rating I want to point user's attention to this stars with very simple way: animate stars highlighting (you could see such behaviour on some ads in real world when each letter is highlighted one after another).
One star highlighted
Two stars highlighted
Three stars highlighted
......
Turn off all of them
So this is the way how I am doing it
func delayWithSeconds(_ seconds: Double, completion: #escaping () -> ()) {
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
completion()
}
}
func ratingStarsAnimation() {
for i in 1...11 {
var timer : Double = 0.6 + Double(i)*0.12
delayWithSeconds(timer) {
ratingStars.rating = (i < 10) ? Double(i) : 0
}
}
}
What is going on here? I have function called delayWithSeconds that delays action and I use this function to delay each star highlighting. And 0.6 is initial delay before animation begins. After all stars are highlighted - last step is to turn off highlighting of all stars.
This code works but I can't say that it is smooth.
My questions are:
How can I change 0.6 + Double(i)*0.12 to get smooth animation feel?
I think that my solution with delays is not good - how can I solve smooth stars highlighting task better?
Have a look at the CADisplaylink class. Its a specialized timer that is linked to the refresh rate of the screen, on iOS this is 60fps.
It's the backbone of many 3rd party animation libraries.
Usage example:
var displayLink: CADisplayLink?
let start: Double = 0
let end: Double = 10
let duration: CFTimeInterval = 5 // seconds
var startTime: CFTimeInterval = 0
let ratingStars = RatingView()
func create() {
displayLink = CADisplayLink(target: self, selector: #selector(tick))
displayLink?.add(to: .main, forMode: .defaultRunLoopMode)
}
func tick() {
guard let link = displayLink else {
cleanup()
return
}
if startTime == 0 { // first tick
startTime = link.timestamp
return
}
let maxTime = startTime + duration
let currentTime = link.timestamp
guard currentTime < maxTime else {
finish()
return
}
// Add math here to ease the animation
let progress = (currentTime - startTime) / duration
let progressInterval = (end - start) * Double(progress)
// get value =~ 0...10
let normalizedProgress = start + progressInterval
ratingStars.rating = normalizedProgress
}
func finish() {
ratingStars.rating = 0
cleanup()
}
func cleanup() {
displayLink?.remove(from: .main, forMode: .defaultRunLoopMode)
displayLink = nil
startTime = 0
}
As a start this will allow your animation to be smoother. You will still need to add some trigonometry if you want to add easing but that shouldn't be too difficult.
CADisplaylink:
https://developer.apple.com/reference/quartzcore/cadisplaylink
Easing curves: http://gizma.com/easing/

Implementing NSTimer in MVVM architecture

I want to implement a NSTimer to show a chronometer using a NSTimeInterval, so I looked around and found this code, which I put into my ViewModel layer:
public class ViewModel {
public func startTimer() {
//if !timer.valid {
timer = NSTimer.scheduledTimerWithTimeInterval(0.01, target: self, selector: #selector(updateTime), userInfo: nil, repeats: true)
startTime = NSDate.timeIntervalSinceReferenceDate()
//}
}
#objc public func updateTime() -> String {
let currentTime = NSDate.timeIntervalSinceReferenceDate()
//Find the difference between current time and start time.
var elapsedTime: NSTimeInterval = currentTime - startTime
//calculate the minutes in elapsed time.
let minutes = UInt8(elapsedTime / 60.0)
elapsedTime -= (NSTimeInterval(minutes) * 60)
//calculate the seconds in elapsed time.
let seconds = UInt8(elapsedTime)
elapsedTime -= NSTimeInterval(seconds)
//add the leading zero for minutes, seconds and millseconds and store them as string constants
let strMinutes = String(format: "%02d", minutes)
let strSeconds = String(format: "%02d", seconds)
//concatenate minuets, seconds and milliseconds as assign it to the UILabel
return "\(strMinutes):\(strSeconds)"
}
}
And I want to show the current elapsed time on my view, so tried this but it didn't work:
viewModel?.startTimer()
timerLabel.text = viewModel?.updateTime()
How can I show the latest result of updateTime() on my ViewController label?
The updateTime method cannot just return the string. It has to initiate the updating of the label in question. You can either code it to update the label directly, or you can provide a closure that updateTime will call when it has the string value.
I tried to implement Rob's answer, but couldn't really get the grasp of CAdisplayLink in a MVVM architecture and came up with the same problem of updating a GUI element periodically in a different view.
However I used my RAC knowledge and created and passed a RACSignal to update my ViewController:
RACSignal.interval(1.0, onScheduler: .mainThreadScheduler()).subscribeNext({ _ in
self.timerLabel.text = self.viewModel?.updateTime()
})

Format realtime stopwatch timer to the hundredth using Swift

I have an app using an NSTimer at centisecond (0.01 second) update intervals to display a running stopwatch in String Format as 00:00.00 (mm:ss.SS). (Basically cloning the iOS built-in stopwatch to integrate into realtime sports timing math problems, possibly needing millisecond accuracy in the future)
I use (misuse?) the NSTimer to force-update the UILabel. If the user presses Start, this is the NSTimer code used to start repeating the function:
displayOnlyTimer = NSTimer.scheduledTimerWithTimeInterval(0.01, target: self, selector: Selector("display"), userInfo: nil, repeats: true)
And here is the function that is executed by the above NSTimer:
func display() {
let currentTime = CACurrentMediaTime() - timerStarted + elapsedTime
if currentTime < 60 {
timeDisplay.text = String(format: "%.2f", currentTime)
}else if currentTime < 3600 {
var minutes = String(format: "%00d", Int(currentTime/60))
var seconds = String(format: "%05.2f", currentTime % 60)
timeDisplay.text = minutes + ":" + seconds
}else {
var hours = String(format: "%00d", Int(currentTime/3600))
var minutes = String(format: "%02d", (Int(currentTime/60)-(Int(currentTime/3600)*60)))
var seconds = String(format: "%05.2f", currentTime % 60)
timeDisplay.text = hours + ":" + minutes + ":" + seconds
}
}
There will be at least 2 display links running at the same time. Will this method be too inefficient once all other elements are in play?
The display is then updated without using NSTimer when the user presses stop/pause/reset. I didn't find anything that directly translated into Swift. I'm fairly certain I'm using an inefficient method to force update the text UILabel quickly in the UIView.
More Details:
I'm working on less messy code for the running timer format (mm:ss.SS). I will update this once more when I've finished that.
UPDATE: Thanks to Rob and jtbandes for answering both of my questions (formatting method and display update method).
It was easy to replace the NSTimer (see above) with CADisplayLink():
displayLink = CADisplayLink(target: self, selector: Selector("display"))
displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes)
And then replace all instances in code of
displayOnlyTimer.invalidate()
with
displayLink.paused = true
(this will pause the display link from updating)
For rapid UI updates you should use a CADisplayLink. Anything faster than the display refresh rate is a waste of processing power since it physically cannot be displayed. It also provides a timestamp of the previous frame so you can try to predict when the next frame will be.
You're calculating CACurrentMediaTime() - timerStarted + elapsedTime multiple times. I would recommend doing it only once and saving it in a local variable.
Consider using NSDateComponentsFormatter. Try to reuse one instance of the formatter rather than creating a new one each time (which is usually the most expensive part). Overall, the less string manipulation you can do, the better.
You can check CACurrentMediaTime at the beginning and end of your display method to see how long it takes. Ideally it should be much less than 16.6ms. Keep an eye on the CPU usage (and general power consumption) in the Xcode debug navigator.
I was solving the same problem today and found this answer. The Rob's and jtbandes' advices are helped a lot and i was able to assemble the clean and working solution from around the internet. Thanks you guys. And thanks to mothy for the question.
I've decided to use CADisplayLink because there is no point in firing timer's callback more often than the screen updates:
class Stopwatch: NSObject {
private var displayLink: CADisplayLink!
//...
override init() {
super.init()
self.displayLink = CADisplayLink(target: self, selector: "tick:")
displayLink.paused = true
displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
//...
}
//...
}
I'm tracking time by incrementing the elapsedTime variable by displayLink.duration each tick:
var elapsedTime: CFTimeInterval!
override init() {
//...
self.elapsedTime = 0.0
//...
}
func tick(sender: CADisplayLink) {
elapsedTime = elapsedTime + displayLink.duration
//...
}
Time-formatting is done through NSDateFormatter:
private let formatter = NSDateFormatter()
override init() {
//...
formatter.dateFormat = "mm:ss,SS"
}
func elapsedTimeAsString() -> String {
return formatter.stringFromDate(NSDate(timeIntervalSinceReferenceDate: elapsedTime))
}
The UI can be updated in the callback closure which Stopwatch calls on every tick:
var callback: (() -> Void)?
func tick(sender: CADisplayLink) {
elapsedTime = elapsedTime + displayLink.duration
// Calling the callback function if available
callback?()
}
And that's all you need to do in the ViewController to utilize the Stopwatch:
let stopwatch = Stopwatch()
stopwatch.callback = self.tick
func tick() {
elapsedTimeLabel.text = stopwatch.elapsedTimeAsString()
}
Here is the gist with the full code of Stopwatch and usage guide:
https://gist.github.com/Flar49/06b8c9894458a3ff1b14
I hope that this explanation and gist will help others who will stumble upon this thread in the future with the same problem :)