Swift creation of separate timers - ios

I am trying to make a countdown timer that connects to a button, and am currently using the
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(ViewController.clock), userInfo: nil, repeats: true)
However, I want it so if I press the same button a new timer starts with the similar settings, so in the console there are 2, 3, etc. countdown timers going at the same time.
How do I make it so whenever I press the button a new timer generates with same settings as the previous but the old one is still active and ticking down?

To keep track of the various timers, you want to create an array of timers ([Timer]) and create a TimerState to pass into each timer as the userInfo object. Then when updateTimer() is called, you can access the state for that timer as timer.userInfo and use it. When a timer reaches 0, call invalidate() and remove it from the list of timers.
Uses the timers array to stop all of the active timers if the user presses the Stop All Timers button.
import UIKit
class TimerState {
let number: Int
var count: Int
init(number: Int, count: Int) {
self.number = number
self.count = count
}
}
class ViewController: UIViewController {
var timerNumber = 1
var startingCount = 10
// Array to hold active timers so that all can be stopped
var timers = [Timer]()
#IBAction func startTimer(_ sender: UIButton) {
let state = TimerState(number: timerNumber, count: startingCount)
let timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(updateTimer), userInfo: state, repeats: true)
timers.append(timer)
timerNumber += 1
}
#objc func updateTimer(_ timer: Timer) {
guard let state = timer.userInfo as? TimerState else { return }
state.count -= 1
if state.count == 0 {
print("Timer \(state.number) is done")
timer.invalidate()
// remove this timer from the list of active timers
if let index = timers.firstIndex(of: timer) {
timers.remove(at: index)
}
} else {
print("Timer \(state.number): \(state.count)")
}
}
#IBAction func stopAllTimers(_ sender: UIButton) {
for timer in timers {
guard let state = timer.userInfo as? TimerState else { continue }
print("Timer \(state.number) stopped")
timer.invalidate()
}
}
}
Pressing the Start Timer button 3 times yields the following output in the console:
Timer 1: 9
Timer 1: 8
Timer 1: 7
Timer 2: 9
Timer 1: 6
Timer 2: 8
Timer 1: 5
Timer 2: 7
Timer 3: 9
Timer 1: 4
Timer 2: 6
Timer 3: 8
Timer 1: 3
Timer 2: 5
Timer 3: 7
Timer 1: 2
Timer 2: 4
Timer 3: 6
Timer 1: 1
Timer 2: 3
Timer 3: 5
Timer 1 is done
Timer 2: 2
Timer 3: 4
Timer 2: 1
Timer 3: 3
Timer 2 is done
Timer 3: 2
Timer 3: 1
Timer 3 is done

Based on your code:
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(ViewController.clock), userInfo: nil, repeats: true)
I would assume that you are declaring timer as an instance variable in ViewController something like:
class ViewController: UIViewController {
var timer: Timer?
#IBAction func tapped(_ sender: Any) {
timer?.invalidate()
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(ViewController.clock), userInfo: nil, repeats: true)
}
#objc func clock() {
print("counting...")
}
}
So each tap on the button, it will schedule a new timing session even if you are using the same Timer instance. In this case, what you should do is to invalidate timer each time before assigning Timer.scheduledTimer to it:
Stops the timer from ever firing again and requests its removal from
its run loop.
In the button action method, add timer?.invalidate() in the first:
#IBAction func tapped(_ sender: Any) {
timer?.invalidate()
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(ViewController.clock), userInfo: nil, repeats: true)
}

Create a new Timer() instance for each button press (if you do indeed want multiple timers) with the same properties.
Would be worth keeping a list of all of your Timers though so you can deal with them when you no longer need them (so you don't have many tens of timers building up if someone spammed the button).

Well, it depends on whether you need to track all of those timers or not. If not is pretty straightforward. Just create a timer each time you click on the button:
func buttonDidClick() {
Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(ViewController.clock), userInfo: nil, repeats: true)
}
if you need to keep track of those timers use an Array:
func buttonDidClick() {
myArray.append(Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(ViewController.clock), userInfo: nil, repeats: true))
}
This way you can even cancel all the timers or do anything you need.

Related

Creating a new instance of Timer when app enters foreground?

I am creating an app where a timer is set by the user. When the app goes to the background, the timer.invalidate(). Now I want the timer to start again when the app comes back to the foreground. I am creating another instance of timer to do it when the app sends notification that app is in the foreground. However, it's not firing the function.
In Viewdidload() I am creating a timer:
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(handleCountdown), userInfo: nil, repeats: true)
RunLoop.current.add(self.timer!, forMode: RunLoop.Mode.common)
And then I have notifications that check if the app is in background or in foreground:
When it enters background I am invalidating the timer.
#objc func applicationDidEnterBackground() {
let defaults = UserDefaults.standard
let quitTime = Date()
defaults.set(quitTime, forKey: "quitTimeKey") //Storing the time of quit in UserDefaults
timer?.invalidate()
}
When the app gets back out, I first check if the timer is isValid or not, and then create a new timer. But this timer doesn't seem to work.
#objc func appEntersForeground() {
calculateTimeLeft()
if let timer = timer {
if (!timer.isValid)
{
Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(handleCountdown), userInfo: nil, repeats: true)
}
}
}
Some help here will be appreciated!
Declare your timer property as weak:
weak var timer: Timer?
Then it will be set to nil when the timer is invalidated. Then just check if timer is nil before creating a new one:
#objc func appEntersForeground() {
calculateTimeLeft()
if timer == nil {
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(handleCountdown), userInfo: nil, repeats: true)
}
}

I need two timers working in the same View Controller ios

I have a problem with two timers running in the same View Controller. I need one of them to launch when the other is invalidated and go back on after the start button for the first is tapped again. I tried creating two variables and it builds successfully, but the behavior is erratic. What would be the right approach? Thanks
#IBAction func Start(_ sender: AnyObject) {
timer2.invalidate()
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(ViewController.update), userInfo: nil, repeats: true)
}
#IBAction func pauseTimer(_ sender: AnyObject) {
timer.invalidate()
timer2 = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: Selector(("increaseTimer")), userInfo: nil, repeats: true)
}
So you need a sort of "flip flop" timer effect?
Hav you thought about replacing var timerHasFinishedRunning: Bool = false with checking the invalidation?
e.g.
// When you invalidate, rather
timer1.invalidate()
timer1 = nil
// As goes for timer2
timer2.invalidate()
timer2 = nil
That way, you can have your timer checking done view computed properties:
var timer1HasFinishedRunning: Bool {
return self.timer1 == nil
}
var timer1HasFinishedRunning: Bool {
return self.timer2 == nil
}
Also you mention that they behave "erratically", could you elaborate? Your timer interval is 1 second, so if anything erratic happens, "within 1 second" it's probably because of the long interval. e.g. the checking of each is only done once per second, so sometimes could take as long as 1.999999 seconds to notice that a timer was invalidated.
Personally, I'd have the interval at 0.1 rather than 1.0 for greater accuracy.

swift invalidate timer doesn't work

I have this problem for a few days now and I don't get what I am doing wrong.
My application is basically just creating some timers. I need to stop them and create new ones. But at the moment stopping them doesn't work.
self.timer = NSTimer.scheduledTimerWithTimeInterval(timeInterval, target:self, selector: "timerDidEnd:", userInfo: "Notification fired", repeats: false)
That's my timer
func timerDidEnd(timer:NSTimer){
createUnrepeatedAlarmWithUpdateInterval()
}
Because my timer didn't want to stop I am currently using the unrepeated timer and start it myself after it stopped.
func stopAlarm() {
if self.timer != nil {
self.timer!.invalidate()
}
self.timer = nil
self.timer = NSTimer()
}
And that's how I stop my timer.
alarmManager.stopAlarm()
alarmManager.createUnrepeatedAlarmWithUpdateInterval()
I call the stopAlarm() function before creating a new timer.
I really don't know what I am doing wrong so I appreciate every answer :)
class AlarmManager: ViewController{
private var timer : NSTimer?
private var unrepeatedTimer : NSTimer?
private let notificationManager = NotificationManager()
private var current = NSThread()
private let settingsViewController = SettingsViewController()
func createRepeatedAlarmWithUpdateInterval(){
var timeInterval:NSTimeInterval = settingsViewController.getUpdateIntervalSettings()
if timer == nil{
timer = NSTimer.scheduledTimerWithTimeInterval(timeInterval,
target: self,
selector: "repeatedTimerDidEnd:",
userInfo: "Notification fired",
repeats: true)
}
}
func repeatedTimerDidEnd(repeatedTimer:NSTimer){
ConnectionManager.sharedInstance.loadTrainings(settingsViewController.getServerSettings())
createUnrepeatedAlarm(10)
}
func createUnrepeatedAlarm(timeInterval:Double){
unrepeatedTimer = NSTimer.scheduledTimerWithTimeInterval(timeInterval,
target: self,
selector: "unrepeatedTimerDidEnd:",
userInfo: "Notification fired",
repeats: false)
}
func unrepeatedTimerDidEnd(unrepeatedTimer:NSTimer){
notificationManager.createNotification(self, reminderType: NotificationManager.ITEMRATINGREMINDER)
notificationManager.createNotification(self, reminderType: NotificationManager.ITEMREMINDER)
print("UnrepeatedAlarm ended")
}
func stopAlarm(){
print("StopAlarm triggered")
if (timer != nil)
{
print("stoptimer executed")
timer!.invalidate()
timer = nil
}
if (unrepeatedTimer != nil)
{
unrepeatedTimer!.invalidate()
unrepeatedTimer = nil
}
}
}
Thats the whole code of this class. Maybe that helps :D
The usual way to start and stop a timer safely is
var timer : Timer?
func startTimer()
{
if timer == nil {
timer = Timer.scheduledTimer(timeInterval: timeInterval, target: self, selector: #selector(timerFired), userInfo: nil, repeats: true)
}
}
func stopTimer()
{
timer?.invalidate()
timer = nil
}
startTimer() starts the timer only if it's nil and stopTimer() stops it only if it's not nil.
You have only to take care of stopping the timer before creating/starting a new one.
Make sure you're calling invalidate on the same thread as the timer.
From the documentation:
Special Considerations
You must send this message from the thread on which the timer was installed. If you send this message from another thread, the input source associated with the timer may not be removed from its run loop, which could prevent the thread from exiting properly.
https://developer.apple.com/documentation/foundation/nstimer/1415405-invalidate?language=objc
Something that's not really covered by the previous answers is that you should be careful your timer isn't scheduled multiple times.
If you schedule a timer multiple times without first invalidating it, it'll end up scheduled on multiple run loops, and invalidating it then becomes nigh impossible.
For me, it happened when calling my scheduleTimer() function in separate functions in my view controller's life cycle (viewWillAppear, viewDidAppear, ...)
So in short, if you aren't sure (or you cannot guarantee) your Timer is only scheduled once, just always invalidate it first.
I have tried every possible solution found but not able to resolve that at the end I have set repeat "false" while initialising timer like below
self.timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(viewcontroller.methodname), userInfo: nil, repeats: false)
And need to add above line in my selector method for whatever the condition for which I wanted to repeat the time.
For example:-
My requirement is I want to repeatedly call some method until one condition satisfied. So instead of adding repeats true I set it false as repeat true does not invalidate timer in my case.
I have added below in my viewdidload method
self.timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(viewcontroller.method), userInfo: nil, repeats: false)
in selector function I added below code
#objc func method{
if condition not matched{
self.timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(viewcontroller.method), userInfo: nil, repeats: false)
}
else{
// once you are here your timer invalidate automatically
}
}
Hope this will solve your problem
For Swift 5 Xcode 12.4 there is example to use timer:
class MyController: UIViewController {
private id: Float;
func setValue(_ value: Float, withAnimation: Bool) {
let step: Float = value / 200
var current: Float = withAnimation ? 0.0 : value
let _ = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: withAnimation) { timer in
DispatchQueue.main.async {
self.id = current
current += step
if current > value || withAnimation == false {
self.id = current
timer.invalidate()
}
}
}
}
}

How to stop NSTimer?

I have a problem with invalidating different timers.
I have multiple timers (NSTimer) on a viewcontroller(settingsVC):
class settingsVC: UIViewController {
// I use 12 timers
var timer1 = NSTimer()
// Seconds to end the timer. Set 12 timers
let timeInterval1:NSTimeInterval = 10
var timer2 = NSTimer()
let timeInterval2:NSTimeInterval = 20
var timer3 = NSTimer()
let timeInterval3:NSTimeInterval = 30
//and so on ... 12 timers
}
With a UIButton (Start) a segue is performed. And for every different value of the variable 'picked', a different timer will be started in the same class:
class settingsVC: UIViewcontroller {
let defaults = NSUserDefaults.standardUserDefaults()
let pickerDefaultsIntegerKey = "Picker" // nsuserdefaults key
#IBAction func start(sender: AnyObject) {
// segue to another viewcontroller
performSegueWithIdentifier("timerOn", sender: self)
if picked == 1 {
defaults.setInteger(1, forKey: pickerDefaultsIntegerKey)
timer1 = NSTimer.scheduledTimerWithTimeInterval(timeInterval1,
target: self,
selector: "timerDidEnd:",
userInfo: nil,
repeats: false)
print("timer1 started")
} else if picked == 2 {
defaults.setInteger(2, forKey: pickerDefaultsIntegerKey)
timer2 = NSTimer.scheduledTimerWithTimeInterval(timeInterval2,
target: self,
selector: "timerDidEnd:",
userInfo: nil,
repeats: false)
print("timer2 started")
} else if // ....and so on{........ }
}
The method fired if timer ends, see selector:
func timerDidEnd(timer:NSTimer){
print("timer ended")
// do other stuff
}
I invalidate the timers with a button (Reset) for values from a variable ('pickerSavedSelection') which is updated by saved values in NSUserdefaults:
#IBAction func reset(sender: AnyObject) {
if let pickerSavedSelection = defaults.integerForKey(pickerDefaultsIntegerKey) as Int?
{
if pickerSavedSelection == 1 {
timer1.invalidate()
} else if pickerSavedSelection == 2 {
timer2.invalidate()
} else if //...and so on{....}
}
All goes well, if I outcomment the perform segue line and just let the user stay on this viewcontroller.The timers get invalidated correctly then:
In the console I read 'timer1 started' and I do NOT read 'timer ended' when the resetButton is pressed.
But staying on this viewcontroller(settingsVC) is NOT the flow of my app.
When the perform segue line is executed and the user 'comes back' to the viewcontroller (settingsVC), the timers are not invalidated when user presses the resetButton:
In the console I read 'timer1 started' and I DO read 'timer ended' when the resetButton is pressed.
How should I stop the timers, when users will 'exit' the viewcontroller and come back to reset the timers?
Help is much appreciated! Thanks in advance
If I am not mistaken at any given point in time, you are only triggering one NSTimer. All your different timers are differentiated only in time intervals. So, my suggestion would be to keep only one NSTimer and have your time interval differentiated. With different value picked you should first invalidate the timer and then restart it with new time interval. That said, your reset will then be much simplified and you do not need to save pickerSavedSelection in NSUserDefaults. This is how I would re-write this code:
class settingsVC: UIViewController {
var timer = NSTimer()
#IBAction func start(sender: AnyObject) {
// segue to another viewcontroller
performSegueWithIdentifier("timerOn", sender: self)
if picked == 1 {
self.timer.invalidate()
self.timer = NSTimer.scheduledTimerWithTimeInterval(10,
target: self,
selector: "timerDidEnd:",
userInfo: nil,
repeats: false)
print("timer1 started")
} else if picked == 2 {
self.timer.invalidate()
self.timer = NSTimer.scheduledTimerWithTimeInterval(20,
target: self,
selector: "timerDidEnd:",
userInfo: nil,
repeats: false)
print("timer2 started")
} else if // ....and so on{........ }
}
#IBAction func reset(sender: AnyObject) {
self.timer.invalidate()
}
}
PS: As a side note, I would advise your NSTimer to start & stop from main thread. Use GCD for that.
It is because your selector is not called when your timer is invalidated, it is called everytime your timer is fired. Since the timer is non-repeat, the selector get called only once. When your press reset button, timer is actually invalidated, you just didn't know because you misunderstood scheduledTimerWithTimeInterval:target:selector:userInfo:repeats: method.

Swift: NSTimer Alternative

I've made a flappy bird clone, but when trying to implement pause functionality, I've come across issues with my NSTimer, which is spawning the pipes.
var timer = NSTimer.scheduledTimerWithTimeInterval(moveSpeed, target: self, selector: Selector("makePipes"), userInfo: nil, repeats: true)
As NSTimers can't be paused, within the makePipes() function I've implemented a simple if-statement, checking whether the game is paused or not, and if it is, not spawning new pipes. However, the gap between each series of pipes is then inconsistent, due to the timer still firing during the paused state.
Also, when transitioning to the game state from the menu, the timer fires off-time, creating the first two pipes in quick succession.
Is there any alternative to the NSTimer to handle this functionality?
You can stop the timer calling the func invalidate() when your game is paused, and then restart it when your game is un-paused.
Update:
You can add a second timer that fires at the difference from the next fire and the time of pause, the second timer should fire the first timer and then reset the first timer to the initial time.
Steps:
Add lets say a timer that fires every 2 seconds
When game is paused, calculate the time interval from the timer.fireDate and timeOfPause, that should be intervalTillNextTrigger
Add a second afterPauseTimer that triggers at intervallTillNextTrigger and should not repeat
When the afterPauseTimer is called, trigger the first timer with timer.fire(), invalidate timer, because timer.fire() will not interrupt it's regular firing schedule, add timer again with 2 seconds firing interval and invalidate afterPauseTimer.
see code below:
//
// ViewController.swift
// swft ios
//
// Created by Marius Fanu on 30/12/14.
// Copyright (c) 2014 Marius Fanu. All rights reserved.
//
import UIKit
class ViewController: UIViewController {
var timer: NSTimer!
var isPaused = false
var isAfterPause = false
var intervalTillNextTrigger: NSTimeInterval = 0
var afterPauseTimer: NSTimer!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
timer = NSTimer(timeInterval: 2, target: self, selector: Selector("timerTriggerd"), userInfo: nil, repeats: true)
NSRunLoop.mainRunLoop().addTimer(timer, forMode: NSDefaultRunLoopMode)
timer.fire()
}
#IBAction func pauseButtonPressed(sender: UIButton) {
var now = NSDate()
println("now = \(now)")
if isPaused == true {
if isAfterPause {
isAfterPause = false
afterPauseTimer = NSTimer(timeInterval: intervalTillNextTrigger, target: self, selector: Selector("timerAfterIntervalTrigger"), userInfo: nil, repeats: false)
NSRunLoop.mainRunLoop().addTimer(afterPauseTimer, forMode: NSDefaultRunLoopMode)
}
timer = NSTimer(timeInterval: 2, target: self, selector: Selector("timerTriggerd"), userInfo: nil, repeats: true)
NSRunLoop.mainRunLoop().addTimer(timer, forMode: NSDefaultRunLoopMode)
}
else {
isAfterPause = true
intervalTillNextTrigger = timer.fireDate.timeIntervalSinceDate(now)
println("till next trigger \(intervalTillNextTrigger)")
timer.invalidate()
timer = nil
}
isPaused = !isPaused
}
func timerTriggerd() {
NSLog("Triggerd!")
}
func timerAfterIntervalTrigger() {
println("reset timer")
timer.fire()
timer.invalidate()
timer = nil
timer = NSTimer(timeInterval: 2, target: self, selector: Selector("timerTriggerd"), userInfo: nil, repeats: true)
NSRunLoop.mainRunLoop().addTimer(timer, forMode: NSDefaultRunLoopMode)
afterPauseTimer.invalidate()
afterPauseTimer = nil
}
}

Resources