Using an Nstimer in Swift

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.

How can I set up timer in Swift?

There are a few issues here:

  • Every time onTimerFires is called, it is decrementing timeHours (which does not appear to be set or used anywhere) and then calls timeFunc, which just grabs values from the picker (which are not changing anywhere) to build the string.

  • This pattern of decrementing a counter in a timer makes a big assumption, namely that the timer is invoked exactly at the right time and that it will not ever miss a timer call. That is not a prudent assumption, unfortunately.

  • I would advise against the Selector based timer, as that introduces a strong reference cycle with the target. The block-based rendition with weak references is easier to avoid these sorts of cycles.

I would suggest a different pattern, namely that you save the time to which you are counting down:

var countDownDate: Date!

func updateCountDownDate() {
let components = DateComponents(hour: pickerView.selectedRow(inComponent: 0),
minute: pickerView.selectedRow(inComponent: 1),
second: pickerView.selectedRow(inComponent: 2))

countDownDate = Calendar.current.date(byAdding: components, to: Date())
}

Then your timer handler can calculate the amount of elapsed time to the target count down date/time:

func handleTimer(_ timer: Timer) {
let remaining = countDownDate.timeIntervalSince(Date())

guard remaining >= 0 else {
timer.invalidate()
return
}

label.text = timeString(from: remaining)
}

When you start the timer, you calculate the countDownDate and schedule the timer:

weak var timer: Timer?

func startTimer() {
timer?.invalidate() // if one was already running, cancel it

updateCountDownDate()
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
guard let self = self else {
timer.invalidate()
return
}

self.handleTimer(timer)
}
timer?.fire()
}

And, for what is worth, when preparing the string, you certainly can determine the date components between to dates (i.e., now and your target date/time), but you can also use a DateComponentsFormatter do this for you:

let formatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .positional
formatter.allowedUnits = [.hour, .minute, .second]
formatter.zeroFormattingBehavior = .pad
return formatter
}()

func timeString(from interval: TimeInterval) -> String? {
return formatter.string(from: interval)
}

How can I make a countdown with NSTimer?

Question 1:

@IBOutlet var countDownLabel: UILabel!

var count = 10

override func viewDidLoad() {
super.viewDidLoad()

var timer = Timer.scheduledTimer(timeInterval: 0.4, target: self, selector: #selector(UIMenuController.update), userInfo: nil, repeats: true)
}

func update() {
if(count > 0) {
countDownLabel.text = String(count--)
}
}

Question 2:

You can do both. SpriteKit is the SDK you use for scene, motion, etc. Simple View Application is the project template. They should not conflict

Using an NSTimer in Swift

You can create a scheduled timer which automatically adds itself to the runloop and starts firing:

Swift 2

NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: "timerDidFire:", userInfo: userInfo, repeats: true)

Swift 3, 4, 5

Timer.scheduledTimer(withTimeInterval: 0.5, target: self, selector: #selector(timerDidFire(_:)), userInfo: userInfo, repeats: true)

Or, you can keep your current code, and add the timer to the runloop when you're ready for it:

Swift 2

let myTimer = NSTimer(timeInterval: 0.5, target: self, selector: "timerDidFire:", userInfo: nil, repeats: true)
NSRunLoop.currentRunLoop().addTimer(myTimer, forMode: NSRunLoopCommonModes)

Swift 3, 4, 5

let myTimer = Timer(timeInterval: 0.5, target: self, selector: #selector(timerDidFire(_:)), userInfo: nil, repeats: true)
RunLoop.current.add(myTimer, forMode: RunLoop.Mode.common)

Pass a Swift function to NSTimer

If you are targeting pre-iOS 10, you can't pass a function to NSTimer because no API was introduced at that time to support closure callbacks.

iOS 10 and later Approach

// swift 2.x users should still use NSTimer instead
Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { timer in
// ...
}

Generic Approach

You can add this class, and reuse it anytime:

final class TimerInvocation: NSObject {

var callback: () -> ()

init(callback: @escaping () -> ()) {
self.callback = callback
}

func invoke() {
callback()
}
}

extension Timer {

static func scheduleTimer(timeInterval: TimeInterval, repeats: Bool, invocation: TimerInvocation) {

Timer.scheduledTimer(
timeInterval: timeInterval,
target: invocation,
selector: #selector(TimerInvocation.invoke(timer:)),
userInfo: nil,
repeats: repeats)
}
}

With this class, you can simply do this now:

let invocation = TimerInvocation {
/* invocation code here */
}

NSTimer.scheduledTimerWithTimeInterval(1, target: invocation, selector:#selector(TimerInvocation.invoke), userInfo: nil, repeats: false)

You don't have to worry about retaining the invocation variable since it is retained by NSTimer

Swift Run An NSTimer Automatically

Swift 2.x :

func DisplayTimer() {
Timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target:self, selector: #selector(updateTimer), userInfo: nil, repeats: true)
}

func updateTimer() {
if Counter != 0 { TimerLabel.text = "\(Counter -= 1)"
} else {
Timer.invalidate()
// call a game over method here...
}
}

override func viewDidLoad() {
super.viewDidLoad()
...

// start the timer when this controller shows up
DisplayTimer()
TimerLabel.text = "\(Counter)"

...
}


Related Topics



Leave a reply



Submit