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)
}
}
Related
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;
}
}
So, I have this timer that is setup to run a specific function (which are both shown below) on a time interval variable called 'frequency' when I try and change the timeinterval variable frequency to a lower number based on the score number it doesn't seem to change the rate at which it fires it just seems to fire at the same time even if the frequency is changed to a lower number
override func didMove(to view: SKView) {
Timer.scheduledTimer(timeInterval: frequency, target: self, selector: #selector(GameScene.spawnFallingOjects), userInfo: nil, repeats: true)
}
func spawnFallingOjects() {
if (GameState.current == .playing || GameState.current == .blackstone) {
guard usingThirdEye == false else { return }
let scoreLabel = childNode(withName: "scoreLabel") as! Score
let lane = [-100, -50 , 0, 50, 100]
let duration = 3.0
switch scoreLabel.number {
case 0...50:
frequency = 6.0
print("frequency has changed: \(frequency)")
case 51...100:
frequency = 4.5
print("frequency has changed: \(frequency)")
case 101...200000:
frequency = 1.1
print("frequency has changed: \(frequency)")
default:
return
}
let randomX = lane[Int(arc4random_uniform(UInt32(lane.count)))]
let object:Object = Object()
object.createFallingObject()
object.position = CGPoint(x: CGFloat(randomX), y: self.size.height)
object.zPosition = 20000
addChild(object)
let action = SKAction.moveTo(y: -450, duration: duration)
object.run(SKAction.repeatForever(action))
}
}
How do I make the timer fire faster when the frequency number changes to a lower number? should I recreate the timer at the end of the function?
You should actually avoid using Timer, Sprite kit has its own time functionality, and Timer does not work well with it and is a real pain to manage.
Instead, use SKAction's to wait and fire:
let spawnNode = SKNode()
override func didMove(to view: SKView) {
let wait = SKAction.wait(forDuration:frequency)
let spawn = SKAction.run(spawnFallingObjects)
spawnNode.run(SKAction.repeatForever(SKAction.sequence([wait,spawn])))
addChild(spawnNode)
}
Then to make it faster, just do:
switch scoreLabel.number {
case 0...50:
spawnNode.speed = 1
print("speed has changed: \(spawnNode.speed)")
case 51...100:
spawnNode.speed = 1.5
print("speed has changed: \(spawnNode.speed)")
case 101...200000:
spawnNode.speed = 2
print("speed has changed: \(spawnNode.speed)")
default:
return
}
The timeInterval property of Timer is a readonly property. (And your code is not trying to write a new frequency into the property...)
should I recreate the timer at the end of the function?
Nearly yes. Just you have no need to do it at the end.
With changing your method header like this:
func spawnFallingOjects(_ timer: Timer) {
You can access the fired Timer through the parameter timer, so you may need to write something like this just after switch scoreLabel.number {...}:
if frequency != timer.timeInterval {
//Invalidate old Timer...
timer.invalidate()
//And then allocate new one
Timer.scheduledTimer(timeInterval: frequency, target: self, selector: #selector(GameScene.spawnFallingOjects), userInfo: nil, repeats: true)
}
You can modify the fireDate property of an existing Timer (in case which still isValid), but recreating a Timer instance is not a heavy operation (comparing to creating an SKSpriteNode instance), so recreating a new Timer seems to be a little bit easier.
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()
})
I'm trying to figure how to call an action when an AVAudioPlayer hits specific second by using NSTimer.
Code:
var audioFile = try! AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("FileName", ofType: "mp3")!))
override func viewDidLoad() {
super.viewDidLoad()
NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: Selector("checkPlaybackTime:"), userInfo: nil, repeats: true)
}
func checkPlaybackTime(timer:NSTimer){
let seconds : NSTimeInterval = audioFile.currentTime
if (seconds == 20.0){
btnPausePlay.setTitle("Play", forState: UIControlState.Normal)
}
}
The action that I wanted to call is that the btnPausePlay button text sets to "Play" which it never does as you see as I've called it in the if statement.
Edit: It now works. I changed from if (seconds == 20.0 to if (seconds >= 20.0)
Although this answer does not make explicit use of NSTimer, it does leverage the method addPeriodicTimeObserverForInterval of the AVPlayer class to achieve the same purpose:
After initialising your player, do this:
let observerInterval = CMTimeMakeWithSeconds(0.5, 100) //this sets the interval at every half second
timeObserverToken = audioPlayer.addPeriodicTimeObserverForInterval(observerInterval, queue: nil, usingBlock: self.getPlaybackTimer)
Function getPlaybackTimer looks like this:
func getPlaybackTimer(time: CMTime){
let currentTime = audioPlayer.currentTime()
/* *** */
}
As nhgrif points out, your logic is flawed. You are comparing floating point values in a way that you should not. You need to take little variations into account, almost always when comparing floating point numbers.
In this case, however, this wouldn't suffice alone, as your comparison might take place every so little outside your checking interval. You are better of by using inequality here, i.e. >= 20.0. You might want to disable the timer then.
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 :)