Having Trouble with Nstimer (Swift)

Trouble with NSTimer in Swift

You're just creating the timer, but not adding it to the run loop. You'll either need to use the equivalent scheduledTimerWithTimeInterval:target:selector:userInfo:repeats: class method or schedule it on the run loop with addTimer:forMode:.

How can I use Timer (formerly NSTimer) in Swift?

This will work:

override func viewDidLoad() {
super.viewDidLoad()
// Swift block syntax (iOS 10+)
let timer = Timer(timeInterval: 0.4, repeats: true) { _ in print("Done!") }
// Swift >=3 selector syntax
let timer = Timer.scheduledTimer(timeInterval: 0.4, target: self, selector: #selector(self.update), userInfo: nil, repeats: true)
// Swift 2.2 selector syntax
let timer = NSTimer.scheduledTimerWithTimeInterval(0.4, target: self, selector: #selector(MyClass.update), userInfo: nil, repeats: true)
// Swift <2.2 selector syntax
let timer = NSTimer.scheduledTimerWithTimeInterval(0.4, target: self, selector: "update", userInfo: nil, repeats: true)
}

// must be internal or public.
@objc func update() {
// Something cool
}

For Swift 4, the method of which you want to get the selector must be exposed to Objective-C, thus @objc attribute must be added to the method declaration.

NSTimer Swift difficulty

The problem has to do with where you are saying this. It looks like you are trying to say this as part of a property declaration:

class ViewController {
var timer = ...
// ...
}

But you can't do that, because there is no self as far as a stored property is concerned. You need to declare the timer as an Optional and then initialize it later:

class ViewController {
var timer = NSTimer!
func someMethod {
timer = ...
}
}

Then you will remove the parentheses (they are wrong) and everything will compile just fine.

Swift Timer Problems

There are various ways to do what you want to do, using a NSTimer is probably the best solution. You can use either a repeating timer (will be invoked repeatedly after an interval) or one without repeating (you will have to reschedule it for every iteration).

The bonus of using a timer instead of dispatch_after is the fact that you can easily cancel (invalidate) a timer, thus avoiding conflicting timers.

Using a timer without repeating:

var timer: NSTimer?
var time: Int = 0

func correct() {
time = 10

//this will remove the previous timer
timer?.invalidate()
startTimer()
}

func startTimer() {
timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: "onTimerFired", userInfo: nil, repeats: false)
}

func onTimerFired() {
time -= 1

if self.time > 0 {
startTimer()
} else {
self.dismissViewControllerAnimated(true, completion: nil)
}
}

Using a timer with repeating:

var timer: NSTimer?
var time: Int = 0

func correct() {
time = 10

//this will remove the previous timer
timer?.invalidate()
startTimer()
}

func startTimer() {
timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: "onTimerFired", userInfo: nil, repeats: true)
}

func onTimerFired() {
time -= 1

if time == 0 {
//need to remove the timer here otherwise it would keep repeating
timer!.invalidate()
self.dismissViewControllerAnimated(true, completion: nil)
}
}

Of course, you can also just use the already running timer, just check if timer != nil && timer!.valid and don't start a new one if the condition is true.

Swift Timer.scheduledTimer() doesn't work

Timers don't work on background queues (without some sleight of hand involving creating run loops or manually scheduling it on an existing run loop). But you should never initiate any UI update from anything other than the main queue, anyway.

So, since you're calling performSegue from a URLSession completion closure (which runs on a background queue), it's actually running viewDidLoad from the background queue, too. Thus the attempt to schedule the timer is failing. To get around this, you have to manually dispatch the performSegue code to the main queue:

let task = URLSession.shared.dataTask(with: url!) { data, response, error in
...

if isPassed != "null" {
DispatchQueue.main.async {
self.performSegue(withIdentifier: "gotoGame", sender: ...)
}
}
}

If you're ever unsure whether some code is running on the main queue or not, refer to the documentation. Or you can use a dispatch precondition:

dispatchPrecondition(condition: .onQueue(.main))

That way it will (in debug builds) stop the app if you've accidentally invoked the code from a background queue.


Unrelated to your current problem, but as an aside, to avoid a strong reference cycle between the timer and the view controller, you generally want to keep a reference to the timer so that you can invalidate it when the view disappears (e.g. create timer in viewDidAppear and remove it in viewDidDisappear). Otherwise you can end up retaining the GameViewController after it was dismissed, e.g.:

class GameViewController: UIViewController {

weak var timer: Timer?

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)

timer = Timer.scheduledTimer(timeInterval: 1.0, target:self, selector: #selector(setCalculationLs(_:)), userInfo: nil, repeats: true)
}

override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)

timer?.invalidate()
}

@objc func setCalculationLs(_ timer: Timer) {
print("Tick")
}
}

Or in iOS 10 or later, you can use the block-based variant with weak reference to self, and invalidate in deinit:

class GameViewController: UIViewController {

weak var timer: Timer?

override func viewDidLoad() {
super.viewDidLoad()

timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] timer in
self?.setCalculationLs()
}
}

deinit {
timer?.invalidate()
}

func setCalculationLs() {
print("Tick")
}

}

NSTimer - how to delay in Swift

Swift 3

With GCD:

let delayInSeconds = 4.0
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + delayInSeconds) {

// here code perfomed with delay

}

or with a timer:

func myPerformeCode() {

// here code to perform
}
let myTimer : Timer = Timer.scheduledTimer(timeInterval: 4, target: self, selector: #selector(self.myPerformeCode), userInfo: nil, repeats: false)

Swift 2

With GCD:

let seconds = 4.0
let delay = seconds * Double(NSEC_PER_SEC) // nanoseconds per seconds
let dispatchTime = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))

dispatch_after(dispatchTime, dispatch_get_main_queue(), {

// here code perfomed with delay

})

or with a timer:

func myPerformeCode(timer : NSTimer) {

// here code to perform
}
let myTimer : NSTimer = NSTimer.scheduledTimerWithTimeInterval(4, target: self, selector: Selector("myPerformeCode:"), userInfo: nil, repeats: false)

NSTimer not working while dragging a UITableView

By using scheduledTimerWithTimeInterval:, as j.tom.schroeder says, your timer is automatically schedule on the main run loop for the default modes. This will prevent your timer from firing when your run loop is in a non-default mode, e.g., when tapping or swiping.

The solution, though, is not using a thread, but scheduling your timer for all common modes:

NSTimer *timer = [NSTimer timerWithTimeInterval:1
target:self
selector:@selector(actualizarTiempo:)
userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

Depending on the kind of events you would like to allow without them stopping your timers, you might also consider UITrackingRunLoopMode. For more info about run loop modes, see Apple Docs.



Related Topics



Leave a reply



Submit