Create a timer that runs each second, on the second? - ios

I have a countdown timer to a specific date. It looks pretty good, and updates each second to show the countdown. Here is the code I'm using to create the timer:
func scheduleTimeLabelsUpdateTimer() {
var components = Calendar.current.dateComponents([.day, .hour, .minute, .second], from: Date())
components.second! += 1
let nextSecondDate = Calendar.current.date(from: components)!
let timer = Timer(fireAt: nextSecondDate, interval: 1, target: self, selector: #selector(updateTimeLabels), userInfo: nil, repeats: true)
RunLoop.main.add(timer, forMode: .commonModes)
}
However, I'd like it to update each second, on the second, so that the clock updates at the same time as each second passes by. Right now it updates each second from when this method is called, which is in viewDidLoad().
For example if the countdown is set for midnight, I want it to hit zero exactly at midnight. Right now it may hit zero slightly after midnight, depending on how far into the second it was when the user opened this screen.
EDIT: This is how the countdown is displayed to the user. updateTimeLabels() just sets the text for each of those labels based on the amount of time left until that date. I would like each of the labels to be updated exactly on each second. This way the countdown will "hit zero" exactly on time. Notice how right now, the number of seconds hits zero, and then the system clock on the status bar updates. I would like these to happen at the same time.
This code, which I found somewhere on Stack Overflow many months ago, is being called in updateTimeLabels() to calculate the remaining time:
public func timeOffset(from date: Date) -> (days: Int, hours: Int, minutes: Int, seconds: Int) {
// Number of seconds between times
var delta = Double(self.seconds(from: date))
// Calculate and subtract whole days
let days = floor(delta / 86400)
delta -= days * 86400
// Caluclate and subtract whole hours
let hours = floor(delta / 3600).truncatingRemainder(dividingBy: 24)
delta -= hours * 3600
// Calculate and subtract whole minutes
let minutes = floor(delta / 60.0).truncatingRemainder(dividingBy: 60)
delta -= minutes * 60
// What's left is seconds
let seconds = delta.truncatingRemainder(dividingBy: 60)
return (Int(days), Int(hours), Int(minutes), Int(seconds))
}

In your code, you never use nanosecond when you init your component
So the timer will always hit at the round second number.
I test your code below print the Date() in the selector
I'm not sure if this is what you are talking about "hit zero", hope this would help

Since a Timer is scheduled on the run loop and other things are happening in the run loop, it isn't particularly accurate; it will fire after the specified interval has passed, but not necessarily exactly when the interval has passed.
Rather than trying to schedule the timer for the desired time, you should run your timer faster than 1 second, say at 0.5 seconds and update your time remaining label each time the timer fires. This will give a smoother update to the display:
var timer: Timer?
func scheduleTimeLabelsUpdateTimer() {
self.timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { (timer) in
self.updateTimeLabels()
}
}
UPDATE
Don't do all of that math; iOS has nice libraries built in to do all of this for you; Simply create your target date and use DateComponents to work it out for you.
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var label: UILabel!
#IBOutlet weak var countDownLabel: UILabel!
let dateFormatter = DateFormatter()
var targetTime: Date!
var calendar = Calendar.current
var timer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
var components = DateComponents()
components.setValue(3, for: .month)
components.setValue(3, for: .day)
components.setValue(2017, for: .year)
components.setValue(0, for: .hour)
components.setValue(0, for: .minute)
components.setValue(1, for: .second)
self.targetTime = calendar.date(from: components)
self.timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { (timer) in
self.updateLabel()
})
self.dateFormatter.timeStyle = .long
self.dateFormatter.dateStyle = .short
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func updateLabel() {
let now = Date()
self.label.text = self.dateFormatter.string(from: now)
let components = calendar.dateComponents([.day,.hour,.minute,.second], from: now, to: self.targetTime)
self.countDownLabel.text = String(format: "%d days %d hours, %d minutes %d seconds", components.day!, components.hour!, components.minute!,components.second!)
}
}

Related

Timer using Swift only counts down for 20 seconds, not twenty minutes

I am creating a timer on my app in iOS which counts down from 20 minutes to 0 seconds, one second at a time. So far the timer works but only counts down for 20 seconds, not minutes. It also doesn't stop when it gets to zero. How can this be resolved?
import UIKit
class SkippingViewController: UIViewController {
#IBOutlet weak var timeLabel: UILabel!
#IBOutlet weak var startWorkoutButton: UIButton!
#IBOutlet weak var pauseWorkoutButton: UIButton!
var timer = Timer()
var counter = 20.00
var isRunning = false
override func viewDidLoad() {
super.viewDidLoad()
timeLabel.text = "\(counter)"
startWorkoutButton.isEnabled = true
pauseWorkoutButton.isEnabled = false
}
#IBAction func startWorkoutButtonDidTouch(_ sender: Any) {
if !isRunning {
timer = Timer.scheduledTimer(timeInterval: -0.01, target: self, selector: #selector(SkippingViewController.updateTimer), userInfo: nil, repeats: true)
startWorkoutButton.isEnabled = false
pauseWorkoutButton.isEnabled = true
isRunning = true
}
}
#IBAction func pauseWorkoutButtonDidTouch(_ sender: Any) {
startWorkoutButton.isEnabled = true
pauseWorkoutButton.isEnabled = false
timer.invalidate()
isRunning = false
}
#objc func updateTimer() {
counter -= 0.01
timeLabel.text = String(format: "%.01f", counter)
}
There's a couple of issues that I see with your code that prevent it from working the way you want.
First of all, if the timeInterval passed in the scheduledTimer method is negative, it will always lead to a timer being created that fires every 0.1 milliseconds (source: documentation).
What you want is your updateTimer to be called every second, so just pass 1.0 to the scheduledTimer method, e.g.:
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(SkippingViewController.updateTimer), userInfo: nil, repeats: true)
Furthermore, you want to invalidate this timer when 20 minutes have passed since starting the timer. So when setting up the timer you could keep track of the current time, and when that time + 20 minutes is more than the current time when called in updateTimer, you can invalidate the timer. With other words, instead of counting down from 20 minutes, we are counting from 0 til 20 minutes have passed!
Example code (didn't try compiling it, but should work, let me know if it doesn't):
import UIKit
class SkippingViewController: UIViewController {
#IBOutlet weak var timeLabel: UILabel!
#IBOutlet weak var startWorkoutButton: UIButton!
#IBOutlet weak var pauseWorkoutButton: UIButton!
var timer = Timer()
var countDownFromMinutes = 20
var timerStartTime: Date?
override func viewDidLoad() {
super.viewDidLoad()
timeLabel.text = "\(countDownFromMinutes):00"
startWorkoutButton.isEnabled = true
pauseWorkoutButton.isEnabled = false
}
#IBAction func startWorkoutButtonDidTouch(_ sender: Any) {
if !timer.isValid {
// run every second
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(SkippingViewController.updateTimer), userInfo: nil, repeats: true)
timerStartTime = nil
startWorkoutButton.isEnabled = false
pauseWorkoutButton.isEnabled = true
}
}
#IBAction func pauseWorkoutButtonDidTouch(_ sender: Any) {
startWorkoutButton.isEnabled = true
pauseWorkoutButton.isEnabled = false
timer.invalidate()
}
#objc func updateTimer() {
guard let startTime = timerStartTime else {
// first firing
timeLabel.text = "\(countDownFromMinutes):00"
timerStartTime = Date()
return
}
let now = Date()
let calendar = Calendar.current
// this ordinarily never returns nil, return gracefully if so
guard let endTime = calendar.date(byAdding: .minute, value: countDownFromMinutes, to: startTime) else {
return
}
let differenceMinuteSeconds = Calendar.current.dateComponents([.minute, .second], from: now, to: endTime)
// 20 minutes have passed since start
if now >= endTime {
timeLabel.text = "00:00"
timer.invalidate()
return
}
timeLabel.text = String(format: "%02d:%02d", differenceMinuteSeconds.minute ?? 0, differenceMinuteSeconds.seconds ?? 0)
}
}
Note that I also replaced the isRunning var you added with using timer.isValid as I believe it would achieve the same without introducing another variable.
Only thing that is probably left is regarding the text of label you are displaying. I am not sure what you actually want to display here. If you can add that in the comments I can suggest an approach for that.
You’ve set your timer’s interval to fire every 0.01 seconds, i.e. 100 times per second. And you’re adjusting the counter by 0.01 each time. So that means that it will decrement the counter at a rate of 1 per second. So a counter of 20 will expire in 20 seconds, not 20 minutes. If you want 20 minutes using your counter mechanism, you’d use 20 * 60.
A couple of other observations:
Timers are not guaranteed to fire at the requested interval. It’s safer to save the time to which you’re counting down (or the time you started) and calculate the time elapsed from that. Then you can update the label with a nice string representation of the time elapsed.
You’re firing your timer every hundredth of a second. But screens don’t generally update with that frequency. Besides, you’re just showing minutes and seconds, so updating more frequently than that offers no benefit.
If I were, though, showing milliseconds, and wanted an optimal timer frequency, I’d use a CADisplayLink rather than a timer. And, in the spirit of point 1, above, if you are using “start”/“stop” date to calculate how much time has elapsed, rather than a counter, you don’t have to worry about what this refresh rate is, since we’re no longer decrementing a counter.

how to update time as per the time from device in status bar?

so I have label that display time in HH:mm format. and that label will be updated periodically in every one minute. I can do that actually by using timer like the code below
class HomeVC: UIViewController {
#IBOutlet var timeLabel: UILabel!
private var timer = Timer()
private var timeBase = Date()
override func viewDidLoad() {
super.viewDidLoad()
setTime()
}
}
extension HomeVC {
//MARK: - Method related to time
func setTime() {
activateTimer()
timeLabel.text = dateTimeService.convert(date: timeBase, toFormat: "HH:mm")
dayLabel.text = dateTimeService.convert(date: timeBase, toFormat: "EEEE")
dateLabel.text = dateTimeService.convert(date: timeBase, toFormat: "d MMMM yyyy")
}
func activateTimer() {
timer = Timer.scheduledTimer(timeInterval: 60 , target: self, selector: #selector(HomeVC.updateTimer), userInfo: nil, repeats: true)
}
#objc func updateTimer() {
// add one minute
timeBase = dateTimeService.addMinuteTo(date: timeBase, minute: 1)
// update label
timeLabel.text = dateTimeService.convert(date: timeBase, toFormat: "HH:mm")
}
}
but it will not exactly the same as the time displayed in the status bar, it will have some seconds difference, how to update the label of time that will always the same as the time displayed in status bar ? so if the time in status bar change, then the label will also change exactly the same
You can use Timer(fireAt:)initializer and pass the next even minute date as the start time, note that when using this initializer you need to add the timer to the main RunLoop for .common modes and make sure you add the initial time the label when setting up your timer:
func activateTimer() {
let now = Date()
let date = Calendar.current.date(bySettingHour: Calendar.current.component(.hour, from: now),
minute: Calendar.current.component(.minute, from: now) + 1,
second: 0,
of: now)!
timer = Timer(fireAt: date, interval: 60, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)
RunLoop.main.add(timer, forMode: .common)
}

Convert seconds into hours in iOS Swift? [duplicate]

I have a label and its value is "03:48".
I want to countdown it like a music player. How can I do that?
03:48 03:47 03:46 03:45 ... 00:00
var musictime =3:48
func stringFromTimeInterval(interval: NSTimeInterval) -> String {
let interval = Int(interval)
let seconds = interval % 60
let minutes = (interval / 60)
return String(format: "%02d:%02d", minutes, seconds)
}
func startTimer() {
var duration=musictime.componentsSeparatedByString(":") //split 3 and 48
var count = duration[0].toInt()! * 60 + duration[1].toInt()! //224 second
timerCounter = NSTimeInterval( count )
NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: "onTimer:", userInfo: nil, repeats: true)
}
#objc func onTimer(timer:NSTimer!) {
// Here is the string containing the timer
// Update your label here
//println(stringFromTimeInterval(timerCounter))
statusLabel.text=stringFromTimeInterval(timerCounter)
timerCounter!--
}
You should take a look at NSDate property timeIntervalSinceNow. All you need is to set a future date as the endDate using NSDate method dateByAddingTimeInterval as follow:
class ViewController: UIViewController {
#IBOutlet weak var timerLabel: UILabel!
var remainingTime: NSTimeInterval = 0
var endDate: NSDate!
var timer = NSTimer()
override func viewDidLoad() {
super.viewDidLoad()
remainingTime = 228.0 // choose as many seconds as you want (total time)
endDate = NSDate().dateByAddingTimeInterval(remainingTime) // set your future end date by adding the time for your timer
timer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: "updateLabel", userInfo: nil, repeats: true) // create a timer to update your label
}
func updateLabel() {
timerLabel.text = endDate.timeIntervalSinceNow.mmss
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
// you will need this extension to convert your time interval to a time string
extension NSTimeInterval {
var mmss: String {
return self < 0 ? "00:00" : String(format:"%02d:%02d", Int((self/60.0)%60), Int(self % 60))
}
var hmmss: String {
return String(format:"%d:%02d:%02d", Int(self/3600.0), Int(self / 60.0 % 60), Int(self % 60))
}
}
Well, by writing code. This is not a "Teach me how to program site. It's a site where you post questions about specific problems you are having with code you have written.
In short, though do the following:
Record your end time
let endInterval = NSDate.timeIntervalSinceReferenceDate() + secondsToEnd
Create a repeating timer that fires once/second.
Each time it fires, compute the number of seconds remaining to endInterval.
Calculate minutes remaining as
Int((endInterval-nowInterval)/60)
Calculate seconds remaining as
Int(endInterval-nowInterval)%60
There is also the new (to iOS 8) class NSDateComponentsFormatter, which I've read a little about but haven't used. I believe that will generate formatted timer intervals like hh:mm:ss for you automatically. You'd use the same approach I outlined above, but instead of calculating minutes and seconds yourself, use the NSDateComponentsFormatter.

Countdown in seconds only (form 00:20 to 0) using NSTimer

I need to have a label countdown from 20secs to 0 and start over again. This is my first time doing a project in Swift and I am trying to use NSTimer.scheduledTimerWithTimeInterval. This countdown should run in a loop for a given amount of times.
I am having a hard time implementing a Start and Start again method (loop). I basically am not finding a way to start the clock for 20s and when it's over, start it again.
I'd appreciate any idea on how to do that
Wagner
#IBAction func startWorkout(sender: AnyObject) {
timer = NSTimer.scheduledTimerWithTimeInterval(0.01, target: self, selector: Selector("countDownTime"), userInfo: nil, repeats: true)
startTime = NSDate.timeIntervalSinceReferenceDate()
}
func countDownTime() {
var currentTime = NSDate.timeIntervalSinceReferenceDate()
//Find the difference between current time and start time.
var elapsedTime: NSTimeInterval = currentTime - startTime
//calculate the seconds in elapsed time.
let seconds = UInt8(elapsedTime)
elapsedTime -= NSTimeInterval(seconds)
//find out the fraction of milliseconds to be displayed.
let fraction = UInt8(elapsedTime * 100)
//add the leading zero for minutes, seconds and millseconds and store them as string constants
let strSeconds = seconds > 9 ? String(seconds):"0" + String(seconds)
let strFraction = fraction > 9 ? String(fraction):"0" + String(fraction)
//concatenate minuets, seconds and milliseconds as assign it to the UILabel
timeLabel.text = "\(strSeconds):\(strFraction)"
}
You should set your date endTime 20s from now and just check the date timeIntervalSinceNow. Once the timeInterval reaches 0 you set it 20 seconds from now again
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var strTimer: UILabel!
var endTime = Date(timeIntervalSinceNow: 20)
var timer = Timer()
#objc func updateTimer(_ timer: Timer) {
let remaining = endTime.timeIntervalSinceNow
strTimer.text = remaining.time
if remaining <= 0 {
endTime += 20
}
}
override func viewDidLoad() {
super.viewDidLoad()
strTimer.font = .monospacedSystemFont(ofSize: 20, weight: .semibold)
strTimer.text = "20:00"
timer = .scheduledTimer(timeInterval: 1/30, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)
}
}
extension TimeInterval {
var time: String {
String(format: "%02d:%02d", Int(truncatingRemainder(dividingBy: 60)), Int((self * 100).truncatingRemainder(dividingBy: 100)))
}
}
In your countdownTime(), change your startTime to current time when your elapsed time reaches 20 seconds
First of all, if you're simply looping without stopping, you could just use module to get the seconds. That is seconds % 20 would just jump from 19.9 to 0.0. And thus, if your counting down, you would calculate seconds - seconds % 20 which would jump to 20 when it reached zero. Over and over again. Is this what you're after?
For the leading zeros you can use: String(format: "%02d:%02d", seconds, fraction). Please note the formatting: here seconds and fraction are integer numbers.
But if you need to stop the timer, you have to keep track of the previously counted seconds and reset the startTime at each start. Every time you stop, you'd have to add up the current seconds to previously counted seconds. Am I making any sense?
To minimize processing, you could create two timers. One timer for 20 seconds and another timer for how often you want to update the UI. It would be difficult to see 100 frames per second. If you are checking every 0.01, your code is less accurate. The manual is really helpful. https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSTimer_Class/ When you no longer have use of the timer invalidate and set to nil. Other timing functions exist also.

NSTimer run every minute but on first second

I have experience with NSTimer to run it once per minute, like
NSTimer.scheduledTimerWithTimeInterval(60.0, target: self, selector: Selector("everyMinute"), userInfo: nil, repeats: true)
Is it possible to use NSTimer, some other class, or some other control, to run some method every minute but on first second of minute ?
I do have some idea how to implement it on my own, but I am first checking is this already exist ?
One way would be to figure out the current next minute using an NSCalendar and schedule the timer to start from that, manually scheduling on the runLoop
let date = NSDate()
let calendar = NSCalendar.currentCalendar()
let components = calendar.components(NSCalendarUnit.CalendarUnitEra|NSCalendarUnit.CalendarUnitYear|NSCalendarUnit.CalendarUnitMonth|NSCalendarUnit.CalendarUnitDay|NSCalendarUnit.CalendarUnitHour|NSCalendarUnit.CalendarUnitMinute, fromDate: date)
components.minute += 1
components.second = 1
let nextMinuteDate = calendar.dateFromComponents(components)
let timer = NSTimer(fireDate: nextMinuteDate!, interval: 60, target: self, selector: Selector("everyMinute"), userInfo: nil, repeats: true)
NSRunLoop.mainRunLoop().addTimer(timer, forMode: NSDefaultRunLoopMode)
The accepted answer, updated for Swift 5:
let date = Date()
let calendar = Calendar.current
var components = calendar.dateComponents([.era, .year, .month, .day, .hour, .minute], from: date)
guard let minute = components.minute else { return }
components.second = 0
components.minute = minute + 1
guard let nextMinute = calendar.date(from: components) else { return }
let timer = Timer(fire: nextMinute, interval: 60, repeats: true) { [weak self] timer in
self?.everyMinute()
}
RunLoop.main.add(timer, forMode: .default)
The NSDate method timeIntervalSinceReferenceDate returns the number of seconds since January 1, 2001, at 12:00 a.m. GMT. It includes fractions of a second
If you call that, divide by 60, take the floor value, then multiply by 60, it should give you the time interval of the current "round minute". Add 60 to that, and you get the time interval for the next "round minute. Add 1 to THAT, and you get a time interval for one second after the next "round minute".
The code might look something like this:
NSTimeInterval now = [NSDate timeIntervalSinceReferenceDate];
NSTimeInterval nextMinute = floor(now/60)*60 + 61 //time interval for next minute, plus 1 second
NSTimeInterval delay = nextMinute - now;
//Delay now contains the number of seconds until the next "round minute", plus 1 second.
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW,
(int64_t)(delay * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^
{
//Replace the code below with whatever target/userInfo you need.
[NSTimer scheduledTimerWithTimeInterval: 60
target: self
userInfo: nil;
repeats: YES];
}
);
Edit: Actually, the "First second" of a minute is the zero'th second, so you should probably change the +61 in the code above that calculated nextMinute to "+60", not "+61"
A much simpler approach:
func everyMinute() {
// some code you want done every minute, in the first second, or VERY close to it
let delaySeconds = 60 - NSDate.timeIntervalSinceReferenceDate.truncatingRemainder(dividingBy: 60)
Timer.scheduledTimer(withTimeInterval: delaySeconds, repeats: false) { timer in
self.everyMinute()
}
}

Resources