I am creating a trivia app in swift and I have a timer that counts down each question. However as the user progresses with each question the timer speeds up. Can someone help me fix this?
My runGameTimer function:
func runGameTimer()
{
gameTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(RockTriviaViewController.updateGameTimer), userInfo: nil, repeats: true)
}
My updateGameTimer function:
#objc func updateGameTimer()
{
gameInt -= 1
timerLabel.text = String(gameInt)
if (gameInt == 0)
{
gameTimer.invalidate()
/*
if (currentQuestion != rockQuestions[questionSet].count)
{
newQuestion()
}
else
{
performSegue(withIdentifier: "showRockScore", sender: self)
}
*/
}
}
Where I call my code:
func newQuestion()
{
gameInt = 11
runGameTimer()
rockQuestion.text = rockQuestions[questionSet][currentQuestion]
rightAnswerPlacement = arc4random_uniform(3)+1
var Button: UIButton = UIButton()
var x = 1
for i in 1...3
{
Button = view.viewWithTag(i) as! UIButton
if(i == Int(rightAnswerPlacement))
{
Button.setTitle(rockAnswers[questionSet][currentQuestion][0], for: .normal)
}
else
{
Button.setTitle(rockAnswers[questionSet][currentQuestion][x], for: .normal)
x = 2
}
}
currentQuestion += 1
}
You're calling runGameTimer() in every call to newQuestion(). If a timer was already running then you'll add a new timer each time, and they will all call your selector. So if you have 3 timers running, your selector will be called 3x as often. That's not what you want.
Change your timer variable to be weak:
weak var gameTimer: Timer?
And then in runGameTimer invalidate the timer before creating a new one, using optional chaining:
func runGameTimer() {
gameTimer?.invalidate() //This will do nothing if gameTimer is nil.
//it will also cause the gameTimer to be nil since it's weak.
gameTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(RockTriviaViewController.updateGameTimer), userInfo: nil, repeats: true)
}
By making the game timer weak it will get set to nil as soon as it's invalidated. (When you schedule a timer the system retains it while it is running so it stays valid as long as it continues to run.)
By using optional chaining to reference the timer:
gameTimer?.invalidate()
The code doesn't do anything if gameTimer is nil.
Related
I want the timer to run even when I close the application. I want it to work in the background counter. the timer goes back one second when I run it.(counter) How can I do that?
class TimerViewController: UIViewController {
var selectedDay: String?
var seconds =
var timer = Timer()
#IBAction func start(_ sender: AnyObject) {
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(TimerViewController.counter), userInfo: nil, repeats: true)
sliderOutlet.isHidden = true
startOutlet.isHidden = true
}
#objc func counter() {
seconds -= 1
favoriteDayTextField.text = String(seconds) + " Seconds"
var bgTask = UIBackgroundTaskIdentifier(rawValue: seconds)
bgTask = UIApplication.shared.beginBackgroundTask(expirationHandler: {
UIApplication.shared.endBackgroundTask(bgTask)
})
if (seconds == 0) {
timer.invalidate()
if self.button.isOn {
updateState()
} else {
updateState1()
}
}
}
}
I am not clear what you want to achieve. Suppose you want to update the label after the timer has started each 1 second. Then one approach will be:-
Start the timer in view did load if the duration is remaining.
Register for applicationWillTerminate
In application will terminate save the passed duration and terminated time to calculate remaining time in next launch.
var remainingDuration: TimeInterval!
override func viewDidLoad() {
super.viewDidLoad()
let remainingDurationFromLastLaunch = UserDefaults.standard.object(forKey: "duration") as? TimeInterval ?? 0
let lastTerminatedTime = UserDefaults.standard.object(forKey: "lastTerminatedDate") as? Date ?? Date()
if Date().timeInterval(since: lastTerminatedTime) > remainingDurationFromLastLaunch {
remainingDuration = remainingDurationFromLastLaunch - Date().timeInterval(since: lastTerminatedTime)
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(TimerViewController.counter), userInfo: nil, repeats: true)
NotificationCenter.default.addObserver(self, selector: #selector(TimerViewController.applicationWillTerminate), name: NSNotification.Name.UIApplicationWillTerminate, object: nil)
} else { //Duration is passed....Do whatever you want
}
}
#objc func counter() {
remainingDuration -= 1
if remainingDuration == 0 { //Duration is passed....Do whatever you want
timer.invalidate()
timer = nil
} else {
favoriteDayTextField.text = String(remainingDuration) + " Seconds"
}
}
#objc func applicationWillTerminate() {
if timer != nil {
backgroundTask = UIApplication.shared.beginBackgroundTask { [weak self] in
UserDefaults.standard.set(remainingDuration, forKey: "duration")
UserDefaults.standard.set(Date(), forKey: "lastTerminatedDate")
}
self?.endBackgroundTask()
}
}
func endBackgroundTask() {
UIApplication.shared.endBackgroundTask(backgroundTask)
backgroundTask = UIBackgroundTaskInvalid
}
The only way for your iOS application to perform some action even while it is in the background is to use Background Modes .
However you cannot perform anything and everything while your
application is in background
There are certain limitations to the type of tasks that you can perform . I have attached a really good article for your reference
Background Modes Tutorial
However, I am not sure if you can initiate and continue a timer sort of functionality while your application is in background
Though, keep in mind , once your application is closed (i.e. by double tapping the home button and swiping the application window up to close it completely) , not even Background modes work at that point because the user does not want to run your app anymore, even in the background
I just started to learn swift . and I found when using a button to start a count down if i pressed the button twice it speeds up the process. What to add to prevent that?
#IBAction func startButton(_ sender: Any) {
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(processTime), userInfo: nil, repeats: true)
}
#objc func processTime(){
if counter > 0 {
counter -= 1
timeLabel.text = "\(counter)"
}else {
timer.invalidate()
}
}
I tried to use sender.isEnabled = false it gave this error (Value of type 'Any' has no member 'isEnabled')
so I did it like this :
#IBAction func startButton(_ sender: Any) {
if timer.isValid != true{
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(processTime), userInfo: nil, repeats: true)
}
}
Many ways, depends on what logic you prefer.
Basically you need to ensure that the timer starts only once until it completes.
In the following example, we start the timer only if it's not been initialized previously.
Furthermore, when we stop the timer, we explicitly set it to nil so the following logic works again after the timer completes.
//globally declared as optional
var timer: Timer?
#IBAction func startButton(_ sender: Any) {
//check if timer is nil
if timer != nil {
//start timer only if timer is nil
timer = Timer.scheduledTimer(timeInterval: 1,
target: self,
selector: #selector(processTime),
userInfo: nil,
repeats: true)
}
}
#objc func processTime() {
if counter > 0 {
counter -= 1
timeLabel.text = "\(counter)"
}
else {
timer.invalidate()
//clear the timer
timer = nil
}
}
add a sender.isEnabled = false and after you press the button once it won't be clickable again
You need to invalidate the timer by calling timer.invalidate() before you reassign a new timer. If you assign a new timer instance to the old one without invalidating, you will lose reference to it and it will never be invalidated.
In a Swift app I am using a Timer. I prefer not to keep a reference to the Timer after I create it and insert it in the Runloop. I want to be able to invalidate it. Is there a way to do this without keeping around a reference?
The timer's selector can keep a reference to the Timer object. Try this:
class ViewController: UIViewController {
var count = 0
override func viewDidLoad() {
super.viewDidLoad()
let _ = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(ViewController.timerFired(timer:)), userInfo: nil, repeats: true)
}
// Run the timers for 3 times then invalidate it
func timerFired(timer: Timer) {
if count < 3 {
count += 1
print(count)
} else {
timer.invalidate()
print("Timer invalidated")
}
}
}
After using
#IBAction func pauseButton(sender: AnyObject) {
if isPaused == false {
timer.invalidate()
isPaused = true
displayLabel.text = "\(count)"
println("App is paused equals \(isPaused)")
} else if isPaused == true {
var isPaused = false
timer.fire()
// timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("updateTime"), userInfo: nil, repeats: true)
}
}
to pause the app, i'd like to hit pause again, in which case, the timer will continue to count from where it left off.
Additionally, when i press pause/play too many times, multiple instances of the timer occur which will cause the timer to increase a few times per second.
Please help!
//
// ViewController.swift
// Navigation Bars
//
// Created by Alex Ngounou on 8/27/15.
// Copyright (c) 2015 Alex Ngounou. All rights reserved.
//
import UIKit
class ViewController: UIViewController {
var timer = NSTimer()
var count = 0
var isPaused = false
func updateTime() {
switch count {
case 0, 1:
count++
println( "\(count) second.")
displayLabel.text = "\(count)"
case 2:
count++
println("\(count) seconds.")
displayLabel.text = "\(count)"
default:
count++
println("\(count) seconds.")
displayLabel.text = "\(count)"
}
}
**strong text**#IBAction func playButton(sender: AnyObject) {
var isPaused = false
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("updateTime"), userInfo: nil, repeats: true)
}
#IBAction func stopButton(sender: AnyObject) {
timer.invalidate()
count = 0
displayLabel.text = "0"
}
// if it's currently paused, pressing on the pause button again should restart the counter from where it originally left off.
#IBAction func pauseButton(sender: AnyObject) {
if isPaused == false {
timer.invalidate()
isPaused = true
displayLabel.text = "\(count)"
println("App is paused equals \(isPaused)")
} else if isPaused == true {
var isPaused = false
timer.fire()
// timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("updateTime"), userInfo: nil, repeats: true)
}
}
#IBAction func resetButton(sender: AnyObject) {
timer.invalidate()
count = 0
displayLabel.text = ""
}
#IBOutlet weak var displayLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("updateTime"), userInfo: nil, repeats: true)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
From: https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSTimer_Class/
Once invalidated, timer objects cannot be reused.
So essentially an NSTimer will do nothing at all once invalidated. Your timer property must be assigned to a newly constructed NSTimer object after that point to get it to fire again. If your invalidation bookkeeping is accurate, there is no "buildup" problem of multiple timers.
Probably the easiest method to your actual problem, though, is logical filtering. That is, keep the NSTimer object around indefinitely and let it fire continually. When the stored property isPaused is true, you ignore timer events (by returning immediately from the processing function), otherwise you process them.
a "lazier" approach can be otherwise useful:
class ViewController: UIViewController {
var timer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
startTimer()
}
final func killTimer(){
self.timer?.invalidate()
self.timer = nil
}
final private func startTimer() {
// make it re-entrant:
// if timer is running, kill it and start from scratch
self.killTimer()
let fire = Date().addingTimeInterval(1)
let deltaT : TimeInterval = 1.0
self.timer = Timer(fire: fire, interval: deltaT, repeats: true, block: { (t: Timer) in
print("hello")
})
RunLoop.main.add(self.timer!, forMode: RunLoopMode.commonModes)
}
Declare your timer as 'weak var' like this:
weak var timer: Timer?
I'm developing a game and I want to create a pause menu. Here is my code:
self.view?.paused = true
but NSTimer.scheduledTimerWithTimeInterval still running...
for var i=0; i < rocketCount; i++ {
var a: NSTimeInterval = 1
ii += a
delaysShow = 2.0 + ((stimulus + interStimulus) * ii)
var time3 = NSTimer.scheduledTimerWithTimeInterval(delaysShow!, target: self, selector: Selector("showRocket:"), userInfo: rocketid[i], repeats: false)
}
I want time3 to pause the timer when player click pause menu and continue run the timer when player come back to the game, but how can I pause NSTimer.scheduledTimerWithTimeInterval? help me please.
You need to invalidate it and recreate it. You can then use an isPaused bool to keep track of the state if you have the same button to pause and resume the timer:
var isPaused = true
var timer = NSTimer()
#IBAction func pauseResume(sender: AnyObject) {
if isPaused{
timer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: Selector("somAction"), userInfo: nil, repeats: true)
isPaused = false
} else {
timer.invalidate()
isPaused = true
}
}
To Start:
timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: Selector("updateView"), userInfo: nil, repeats: true)
To Resume:
timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: Selector("updateView"), userInfo: nil, repeats: true)
To Pause:
timer.invalidate
This worked for me. The trick is that do not look for something like "timer.resume" or "timer.validate". Just use "the same code for starting a timer" to resume it after the pause.
To start
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(ViewController.action), userInfo: nil, repeats: true)
To pause
timer.invalidate()
To reset
time += 1
label.text = String(time)
'label' is the timer on output.
I was just working through a similar problem with my game, and found an easy solution.
First I should point out like others have, that Timer and NSTimer doesn't have a pause function. You have to stop the Timer with Timer.invalidate(). After invalidating a Timer, you must initialize it again to start the Timer. To quote from https://developer.apple.com/documentation/foundation/timer, the function .invalidate() -
Stops the timer from ever firing again and requests its removal from
its run loop.
To pause a timer we can use Timer.fireDate, this is where Timer (and NSTimer) saves the date for when the Timer will fire in the future.
Here's how we can pause a Timer by saving the seconds left that the Timer has until it fires again.
//The variable we will store the remaining timers time in
var timeUntilFire = TimeInterval()
//The timer to pause
var gameTimer = Timer.scheduledTimer(timeInterval: delaysShow!, target: self, selector: #selector(GameClass.showRocket), userInfo: rocketid[i], repeats: false)
func pauseTimer()
{
//Get the difference in seconds between now and the future fire date
timeUntilFire = gameTimer.fireDate.timeIntervalSinceNow
//Stop the timer
gameTimer.invalidate()
}
func resumeTimer()
{
//Start the timer again with the previously invalidated timers time left with timeUntilFire
gameTimer = Timer.scheduledTimer(timeInterval: timeUntilFire, target: self, selector: #selector(GameClass.showRocket), userInfo: rocketid[i], repeats: false)
}
Note: Don't invalidate() the Timer before getting the fireDate. After invalidate() is called the Timer seems to reset the fireDate to 2001-01-01 00:00:00 +0000.
2nd Note: A timer can potentially fire after its set fireDate. This will lead to a negative number, which will default the Timer to run after 0.1 milliseconds instead. https://developer.apple.com/documentation/foundation/timer/1412416-scheduledtimer
To stop it
time3.invalidate()
To start it again
time3.fire()
You can not resume the timer back.
Instead of resuming - just create a new timer.
class SomeClass : NSObject { // class must be NSObject, if there is no "NSObject" you'll get the error at runtime
var timer = NSTimer()
init() {
super.init()
startOrResumeTimer()
}
func timerAction() {
NSLog("timer action")
}
func pauseTimer() {
timer.invalidate
}
func startOrResumeTimer() {
timer = NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: Selector("timerAction"), userInfo: nil, repeats: true)
}
}
Even though the solutions exposed here are good, I thought an important insight was missing. Like a lot of people here explained, timer invalidate() and recreate is the best option. But one could argue that you could do something like this:
var paused:Bool
func timerAction() {
if !paused {
// Do stuff here
}
}
is easier to implement, but it will be less efficient.
For energy impact reasons, Apple promotes avoiding timers whenever possible and to prefer event notifications. If you really need to use a timer, you should implement pauses efficiently by invalidating the current timer. Read recommendations about Timer in the Apple Energy Efficiency Guide here:
https://developer.apple.com/library/content/documentation/Performance/Conceptual/EnergyGuide-iOS/MinimizeTimerUse.html
SWIFT3
Global Declaration :
var swiftTimer = Timer()
var count = 30
var timer = Timer()
#IBOutlet weak var CountDownTimer: UILabel!
viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
BtnStart.tag = 0
}
Triggered IBACTION :
#IBAction func BtnStartTapped(_ sender: Any) {
if BtnStart.tag == 0 {
BtnStart.setTitle("STOP", for: .normal)
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(ScoreBoardVC.update), userInfo: nil, repeats: true)
BtnStart.tag = 1
} else {
BtnStart.setTitle("START", for: .normal)
timer.invalidate()
BtnStart.tag = 0
}
}
Function that Handles The things :
func update(){
if(count > 0){
let minutes = String(count / 60)
let ConvMin = Float(minutes)
let minuttes1 = String(format: "%.0f", ConvMin!)
print(minutes)
let seconds = String(count % 60)
let ConvSec = Float(seconds)
let seconds1 = String(format: "%.0f", ConvSec!)
CountDownTimer.text = (minuttes1 + ":" + seconds1)
count += 1
}
}
Pause timer : timer.invalidate()
and
Resume timer : recreate timer. it's work fine.
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(mainController.updateTimer), userInfo: nil, repeats: true)