NSTimer Too Slow - ios

I have seen multiple questions similar to this; however, none of them resolved my problem. My timer is simply going too slowly. I am only using an interval of 0.01 seconds. Here is my code:
#IBOutlet var timerLabel: UILabel!
var miliseconds = 0
var seconds = 0
func updateLabel() {
if miliseconds == 0 {
timerLabel.text = "\(seconds).00"
} else if miliseconds < 10 {
timerLabel.text = "\(seconds).0\(miliseconds)"
} else {
timerLabel.text = "\(seconds).\(miliseconds)"
}
}
var timer = NSTimer()
func updateTime() {
miliseconds++
if miliseconds == 100 {
miliseconds = 0
seconds++
}
updateLabel()
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
if timerState == 1 {
timer = NSTimer.scheduledTimerWithTimeInterval(0.01, target: self, selector: "updateTime", userInfo: nil, repeats: true)
NSRunLoop.mainRunLoop().addTimer(timer, forMode: NSRunLoopCommonModes)
timerLabel.textColor = UIColor.blackColor()
timerState = 2
}
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if timerState == 0 {
miliseconds = 0
seconds = 0
updateLabel()
timerLabel.textColor = UIColor.greenColor()
timerState = 1
} else if timerState == 2 {
timerState = 0
timer.invalidate()
}
}
var timerState = 0
//timerState of 0 = Has not started
//timerState of 1 = About to start
//timerState of 2 = Timing
I have also tried using delays:
func delay(delay:Double, closure:()->()) {
dispatch_after(
dispatch_time( DISPATCH_TIME_NOW, Int64(delay * Double(NSEC_PER_SEC))), dispatch_get_main_queue(), closure)
}
I called updateTime in viewDidLoad, and at the end of updateTime, I added:
delay(0.01) { () -> () in
self.updateTime()
}
However, it still went at the same speed as before.
How can I fix this issue? If I missed a question while researching, please let me know. Thanks!

There is a whole mess of problems here. Let's clean this up.
class ViewController: UIViewController {
#IBOutlet var timerLabel: UILabel!
First of all, don't use a timer to update the label. Use a CADisplayLink. A display link synchronizes with the screen refresh interval, which is 1/60 of a second on most iOS devices (not 1/100), so you don't do extra work:
private var displayLink: CADisplayLink?
Next, don't try to track the elapsed time by incrementing a counter when the timer (or link) fires, because the timer or link is not guaranteed to fire as often as you requested. Instead, store the time at which the timer was started, and the time at which it was stopped (if it was stopped):
private var startTime: CFAbsoluteTime = 0
private var endTime: CFAbsoluteTime = 0 {
didSet {
updateLabel()
}
}
Track the state using an enum instead of mysterious hard-coded numbers. And since the label color depends only the state, add a property to the state that gives the label color for that state:
private enum State {
case Stopped
case Pending
case Running
var labelColor: UIColor {
switch self {
case .Pending: return UIColor.greenColor()
case .Stopped, .Running: return UIColor.blackColor()
}
}
}
The elapsed time depends on the state, so add a method to compute it:
private var elapsedTime: NSTimeInterval {
switch state {
case .Stopped: return endTime - startTime
case .Running: return CFAbsoluteTimeGetCurrent() - startTime
case .Pending: return 0
}
}
Use a format string to convert the elapsed time to a string when updating the label:
private func updateLabel() {
timerLabel.text = String(format: "%.02f", elapsedTime)
}
Changing the timer state can change both the label color and the elapsed time, so update the label color and the label text when the state changes:
private var state = State.Stopped {
didSet {
timerLabel.textColor = state.labelColor
updateLabel()
}
}
When a touch begins, create the display link if needed, then update the state. The state's didSet will handle updating the label as necessary:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
createDisplayLinkIfNeeded()
switch state {
case .Stopped:
state = .Pending
case .Pending:
break
case .Running:
state = .Stopped
endTime = CFAbsoluteTimeGetCurrent()
displayLink?.paused = true
}
}
When a touch ends, start the timer if necessary:
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
if state == .Pending {
startTime = CFAbsoluteTimeGetCurrent()
displayLink?.paused = false
state = .Running
}
}
Here's how you create the display link:
private func createDisplayLinkIfNeeded() {
guard self.displayLink == nil else { return }
let displayLink = CADisplayLink(target: self, selector: "displayLinkDidFire:")
displayLink.paused = true
displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
self.displayLink = displayLink
}
And here's the method the display link will call:
func displayLinkDidFire(_: CADisplayLink) {
updateLabel()
}
} // end of ViewController

From my comment above ... (with encouragement)
NSTimer - from apple developer docs:
"A timer is not a real-time mechanism; it fires only when one of the
run loop modes to which the timer has been added is running and able
to check if the timer’s firing time has passed. Because of the various
input sources a typical run loop manages, the effective resolution of
the time interval for a timer is limited to on the order of 50-100
milliseconds. "

Related

Carrying Elapsed Time over to another ViewController

I have a small elapsed timer in my game and it works very well. However I am trying to figure out how to save the elapsed time when you die so I can carry it over to the Game Over Screen where the Score and High Score is displayed.
I tired a few things but none of them seem to work. I guess it's because the time is not being saved anywhere when the it's game over, but rather just reset to 00:00:00 when the game restarts.
I use two view Controllers for this timer. One is called Stopwatch the other code is in the GameScene. Here are the codes.
I wanna bring it into a label like for example:
let timeLabel = SKLabelNode(fontNamed: "Planer")
timeLabel.text = "Time: \(savedTimer)"
timeLabel.fontSize = 100
timeLabel.fontColor = SKColor.white
timeLabel.zPosition = 2
timeLabel.position = CGPoint (x: self.size.width/2, y: self.size.height * 0.5)
self.addChild(timeLabel)*/
Stopwatch.swift code
import Foundation
class Stopwatch {
private var startTime : Date?
var elapsedTime: TimeInterval {
if let startTime = self.startTime {
return -startTime.timeIntervalSinceNow
} else {
return 0
}
}
var isRunning: Bool {
return startTime != nil
}
func start() {
startTime = Date()
}
func stop() {
startTime = nil
}
}
And the code I got speed out through my Game Scene:
import UIKit
class ViewController: UIViewController {
private let formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "mm:ss:SS"
return formatter
}()
let watch = Stopwatch()
#IBOutlet weak var elapsedTimeLabel: UILabel!
#IBAction func startButton(_ sender: Any) {
Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(self.updateElapsedTimeLabel), userInfo: nil, repeats: true)
watch.start()
}
#IBAction func stopButton(_ sender: Any) {
watch.stop()
}
#objc func updateElapsedTimeLabel (timer : Timer) {
if watch.isRunning {
elapsedTimeLabel.text = formatter.string(from: Date(timeIntervalSince1970: watch.elapsedTime))
} else {
timer.invalidate()
}
}
override func viewDidLoad() {
super.viewDidLoad()
}
override var prefersStatusBarHidden: Bool {
return true
}
}
What I understand is that you're trying to save the elapsedTime of your watch after the user taps the stop button. If that's the case, in your stopButton function you are calling watch.stop(), which in turn resets the startTime = nil. So you might want to edit it like so:
// Create a new class variable to store the time
var savedTime: TimeInterval? = nil
#IBAction func stopButton(_ sender: Any) {
savedTime = watch.elapsedTime
// Use the savedTime here to pass to the game over function
watch.stop()
}
If you don't need to save the time in your ViewController class, you can move the savedTime variable to a local one in the stopButton function.

When I pause my timer, then try to start it again, it does not run

I'm building an app in Swift 3. When I press start the first time my timer begins, but when I pause it and try to press start again, the timer does not budge. To give context, the timer, with an amount of time attached to it, is selected from a table. each time the timer load, the start button works initially.
protocol TimerViewControllerDelegate: class {
func viewController(_ controller: ViewController, didFinishEditing item: TaskData)
}
class ViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var timerLabel: UILabel!
#IBOutlet weak var pauseButton: UIButton!
#IBOutlet weak var startButton: UIButton!
#IBOutlet weak var timerTaskName: UILabel!
#IBOutlet weak var timerTimeSetting: UILabel!
#IBOutlet weak var progressView: UIProgressView!
weak var delegate: TimerViewControllerDelegate?
var timerTask: TaskData?
var timer: Timer?
var progressViewSpeed: Double = 0.0
#IBAction func cancel(_ sender: Any) {
timer?.invalidate()
dismiss(animated: true, completion: nil)
delegate?.viewController(self, didFinishEditing: timerTask!)
}
#IBAction func startButtonTapped(_ sender: Any) {
timerTask?.startTime = Date()
runTimer()
if timerTask?.isTaskRunning == true {
runTimer()
self.startButton.isEnabled = false
self.pauseButton.isEnabled = true
} else {
//retrieve start time and run
timerTask?.startTime = Date()
runTimer()
self.startButton.isEnabled = false
self.pauseButton.isEnabled = true
}
}
func runTimer() {
guard timer == nil else {
return
}
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: (#selector(ViewController.updateTimer)), userInfo: nil, repeats: true)
}
#IBAction func pauseButtonTapped(_ sender: UIButton) {
if timerTask?.isTaskRunning == true {
timer?.invalidate()
if let timerTask = timerTask, timerTask.isTaskRunning {
// Calculate the difference between now and when the timerTask was started
let difference = Int(Date().timeIntervalSince(timerTask.startTime!))
timerTask.taskRemaining -= difference
if timerTask.taskRemaining == 0 {
// Do something when there's no time remaining on the task?
}
timerTask.startTime = nil
}
}
else {
timerTask?.startTime = Date()
runTimer()
self.pauseButton.setTitle("Pause",for: .normal)
}
self.startButton.isEnabled = true
self.pauseButton.isEnabled = false
}
/*
#IBAction func resetButtonTapped(_ sender: Any) {
timer.invalidate()
seconds = 60
self.timerLabel.text = timeString(time: TimeInterval(seconds))
if self.resumeTapped == true {
self.resumeTapped = false
self.pauseButton.setTitle("Pause",for: .normal)
}
isTimerRunning = false
pauseButton.isEnabled = false
startButton.isEnabled = true
}
*/
func updateTimer() {
guard let timerTask = timerTask else {
return
}
if timerTask.taskRemaining < 1 {
timer?.invalidate()
timer = nil
//Send alert to indicate "time's up!"
} else {
updateTime()
}
progressViewSpeed = 1 / Double(timerTask.taskRemaining)
progressView.progress += Float(progressViewSpeed)
}
func timeString(time:TimeInterval) -> String {
let hours = Int(time) / 3600
let minutes = Int(time) / 60 % 60
let seconds = Int(time) % 60
return String(format:"%02i:%02i:%02i", hours, minutes, seconds)
}
override func viewDidLoad() {
super.viewDidLoad()
guard let timerTask = timerTask else {
return
}
if timerTask.isTaskRunning {
startButton.isEnabled = false
pauseButton.isEnabled = true
runTimer()
} else {
startButton.isEnabled = true
pauseButton.isEnabled = false
}
timerTaskName.text = timerTask.task
updateTime()
self.progressView.transform = CGAffineTransform.identity.rotated(by: CGFloat.pi / 2).scaledBy(x: 1, y: 150)
}
func updateTime() {
guard let timerTask = timerTask else {
return
}
if let startTime = timerTask.startTime {
// Calculate the difference between now and when the timerTask was started
let difference = Int(Date().timeIntervalSince(startTime))
if timerTask.taskRemaining == difference {
// Do something when there's no time remaining on the task
timer?.invalidate()
timer = nil
}
timerLabel.text = timeString(time: TimeInterval(timerTask.taskRemaining - difference))
} else {
timerLabel.text = timeString(time: TimeInterval(timerTask.taskRemaining))
}
}
}
Once you've invalidated an NSTimer, you can't use it again. You should create the new object.
See here for more From NSTimer Docs
Calling this method requests the removal of the timer from the current run loop; as a result, you should always call the invalidate method from the same thread on which the timer was installed. Invalidating the timer immediately disables it so that it no longer affects the run loop. The run loop then removes and releases the timer, either just before the invalidate method returns or at some later point. Once invalidated, timer objects cannot be reused.
You need to invalidate it and recreate it. "isPaused" bool to keep track of the state
var isPaused = true
var timer: Timer?
#IBAction func pauseResume(sender: AnyObject) {
if isPaused{
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: (#selector(ViewController.updateTimer)), userInfo: nil, repeats: true)
pauseButton.isHidden = false
startButton.isHidden = true
isPaused = false
} else {
pauseButton.isHidden = true
startButton.isHidden = false
timer.invalidate()
isPaused = true
}
}

Continuously check for response in Swift

I have a boolean variable called flag with initial value of false. Based on a successful process, it's set to true. There is a button alert, when tap it, it checks for flag's value along with a spinning image on UI, if flag is true, then a success message should displayed. otherwise, it should keep continuing response check (ten times for 5 seconds).
This is my functionality. I've been using NStimer to achieve this. Here is the code snippet:
var timer = NSTimer()
var count = 10
var flag: Bool = false
#IBOutlet weak var alert: UIButton!
#IBAction func alertAction(sender: AnyObject) {
timer = NSTimer.scheduledTimerWithTimeInterval(5, target: self, selector: #selector(ViewController.prints), userInfo: nil, repeats: true)
}
func prints(){
if(count > 0)
{
if flag == false{
**Spinning Image**
count -= 1
} else {
count = 0
}
} else {
timer.invalidate()
}
}
The spinning image stops and continues after every 5 seconds ( in case response takes more than 5 seconds). I wish to spin the image continuously without a break. Can someone please help?
Thanks in advance!
Polling is the most desperate asynchronous pattern and almost always wrong. Learn a bit about value observation and reactive pattern.
var flag = false {
didSet {
if flag {
// stop spinning
}
}
}
func alertAction() {
// start spinning
}
From what I understand, this code will do what you intend. If you are using a UIActivityIndicator. Ensure to start it where I started the rotationAnimation and stop it when invalidating your timer.
Swift 3 Example
#IBOutlet weak var pin: UIImageView!
var timer: Timer?
var count: Int = 5
var flag: Bool {
return count == 0
}
#IBAction func buttonPressed(_ sender: AnyObject) {
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotationAnimation.fromValue = 0
rotationAnimation.toValue = 2 * M_PI
rotationAnimation.duration = 0.6
rotationAnimation.isCumulative = true
rotationAnimation.repeatCount = Float.infinity
pin.layer.add(rotationAnimation, forKey: "rotate")
timer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(prints), userInfo: nil, repeats: true)
}
func prints() {
if flag {
pin.layer.removeAllAnimations()
timer?.invalidate()
} else {
count = count - 1
}
}

Stopwatch app counter going too fast

I am just learning iOS and programming in general and I am making a very basic iOS stopwatch app. I got the stopwatch working however, when I press start more than once the timer begins to go faster so that it is no longer a second long (gif here). Also, my formatting seems to be off for the seconds part, if you have any suggestions there it would be appreciated. Here is my code:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var minuteLabel: UILabel!
#IBOutlet weak var secondLabel: UILabel!
var timer = NSTimer()
var second = 0
var minute = 0
func updateTime() {
do{
if second != 59
{
second++
secondLabel.text = ".\(second)"
}
else
{
second = 0
minute++
secondLabel.text = "." + String(format:"$%.2f", second)
if minute < 10
{
minuteLabel.text = "0\(minute)"
}
else
{
minuteLabel.text = String(format:"$%.2f", minute)
}
}
}
}
#IBAction func resetButton(sender: AnyObject) {
timer.invalidate()
second = 0
minute = 0
secondLabel.text = ".00"
minuteLabel.text = "00"
}
#IBAction func stopButton(sender: AnyObject) {
timer.invalidate()
}
#IBAction func startButton(sender: AnyObject) {
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("updateTime"), userInfo: nil, repeats: true)
}
Thank you for your help!
You are not invalidating timer when startButton is called, so tapping "Start" multiple times is creating duplicate timers which call the same function, updateTime. Change startButton to look like this:
#IBAction func startButton(sender: AnyObject) {
if !timer.valid
{
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("updateTime"), userInfo: nil, repeats: true)
}
}
For your second question about the formatting, you need to put a condition to check for the seconds being less than 10, similar to what you did with the minutes. You would put a 0 in front of the seconds. In updateTime:
if second < 10
{
second++
secondLabel.text = ".0\(second)"
}
else if second <= 59
{
second++
secondLabel.text = ".\(second)"
}
else
{
...
}
See NSTimer documentation for more information.
You can get the desired formatting by using "%02d" instead of %.2f. Using %.2f will ensure 2 digits after the decimal point which is not true in your case (you have both second and minute as integers). Also you can use get away with using one label (e.g., timerLabel) as shown below:
var second = 0
var minute = 0
func updateTime() {
let dFormat = "%02d"
second++
if second == 59{
minute++
second = 0
}
let s = "\String(format: dFormat, minute):\String(format: dFormat, second))"
timerLabel.text = s
}
Hope this helps!

Use NSTimeInterval to trigger notification in WatchOS2

I am trying to make a pomodoro app on the Apple Watch (OS2). I want to trigger a notification after the countdown is finished, in the first step I am trying to print some word in console, but it still does not works. How to use NSTimeInterval to get the remaining time to do that?
import WatchKit
import Foundation
class InterfaceController: WKInterfaceController {
let countdown:NSTimeInterval = 1501
var timerRunning = false
#IBOutlet var pauseButton: WKInterfaceButton!
#IBOutlet var timer: WKInterfaceTimer!
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
}
override func willActivate() {
super.willActivate()
}
override func didDeactivate() {
super.didDeactivate()
}
#IBAction func startPomodoro() {
let date = NSDate(timeIntervalSinceNow: countdown)
timer.setDate(date)
timer.start();
WKInterfaceDevice.currentDevice().playHaptic(.Start)
WKInterfaceDevice.currentDevice().playHaptic(.Start)
}
#IBAction func resetPomodoroTimer() {
timer.stop()
let resetCountdown:NSTimeInterval = 1501
let date = NSDate(timeIntervalSinceNow: resetCountdown)
timer.setDate(date)
WKInterfaceDevice.currentDevice().playHaptic(.Retry)
WKInterfaceDevice.currentDevice().playHaptic(.Retry)
}
#IBAction func pausePomodoro() {
timer.stop()
if !timerRunning{
pauseButton.setTitle("Restart")
}
WKInterfaceDevice.currentDevice().playHaptic(.Stop)
WKInterfaceDevice.currentDevice().playHaptic(.Stop)
}
func showNotification(){
if countdown < 1490
{
print("Notification")
WKInterfaceDevice.currentDevice().playHaptic(.Success)
WKInterfaceDevice.currentDevice().playHaptic(.Success)
}
}
}
To trigger a notification after the WKInterfaceTimer has finished you have to add a NSTimer with the same time interval. You start both at the same time and when the NSTimer fires you know that the WKInterfaceTimer also finished. IMHO that is not a very elegant solution, but it is suggested in Apple's documentation, so there's apparently is no other way to do this.
If you want to add a pause / restart functionality you have to track the remaining time when the user hits "pause" and stop both timers. When the user starts the timer again, you set both timers to the remaining time and start them.
Here is a working example with pause / restart functionality (it has a WKInterfaceButton that is connected to the button outlet and the didPressButton: action:
enum TimerState {
case Idle, Running, Paused, Finished
}
class InterfaceController: WKInterfaceController {
let countdownDuration: NSTimeInterval = 10
var remainingDuration: NSTimeInterval = 10
var timer: NSTimer?
var timerState = TimerState.Idle
#IBOutlet var interfaceTimer: WKInterfaceTimer!
#IBOutlet var button: WKInterfaceButton!
#IBAction func didPressButton() {
switch timerState {
case .Idle:
startTimer(remainingDuration: countdownDuration)
case .Running:
let fireDate = timer!.fireDate
remainingDuration = fireDate.timeIntervalSinceDate(NSDate())
interfaceTimer.stop()
timer?.invalidate()
button.setTitle("Continue")
timerState = .Paused
case .Paused:
startTimer(remainingDuration: remainingDuration)
case .Finished:
break
}
}
func startTimer(remainingDuration duration:NSTimeInterval) {
interfaceTimer.setDate(NSDate(timeIntervalSinceNow: duration))
interfaceTimer.start()
timer = NSTimer.scheduledTimerWithTimeInterval(duration, target: self, selector: Selector("timerDidFire:"), userInfo: nil, repeats: false)
button.setTitle("Pause")
timerState = .Running
}
func timerDidFire(timer: NSTimer) {
interfaceTimer.stop()
timerState = .Finished
WKInterfaceDevice.currentDevice().playHaptic(.Success)
}
}

Resources