I want to create a 24 hour sale timer and display it inside button. I am using this code but don't know how to stop this timer.
This is my code. In this example I use 35 second to test this faster:
class SLTimer {
func timeLeftExtended(date: Date) -> String {
let cal = Calendar.current
let now = Date()
let calendarUnits:NSCalendar.Unit = [NSCalendar.Unit.hour, NSCalendar.Unit.minute, NSCalendar.Unit.second]
let components = (cal as NSCalendar).components(calendarUnits, from: now, to: date, options: [])
let fullCountDownStr = "\(components.hour!.description) : " +
"\(components.minute!.description) : " +
"\(components.second!.description)"
return fullCountDownStr
}
@objc func updateCountDown() -> String
{
var timeString = ""
let newDate = Calendar.current.date(byAdding: .second, value: 35, to: Date())
if let waitingDate = UserDefaults.standard.value(forKey: "waitingDate") as? Date {
if let diff = Calendar.current.dateComponents([.second], from: newDate!, to: Date()).second, diff > 35 {
timeString = "stop"
} else {
timeString = self.timeLeftExtended(date: waitingDate)
}
} else {
UserDefaults.standard.set(newDate, forKey: "waitingDate")
timeString = self.timeLeftExtended(date: newDate!)
}
return timeString
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(self.start), userInfo: nil, repeats: true)
}
@objc func start() {
saleButton.setTitle("\(SLTimer().updateCountDown())", for: .normal)
}
}
it doesn't call this lines after 35 seconds:
if let diff = Calendar.current.dateComponents([.second], from: newDate!, to: Date()).second, diff > 35 {
timeString = "stop"
}
You need to keep a reference to the Timer
you create. For smoother output I would suggest running the timer faster than 1 second, as Timer
s can be jittery.
Also, your start
function is creating a new instance of SLTimer
each time. You should do this once and keep the reference in a property.
Finally, It is more "Swifty" to use the Timer
initialiser that accepts a closure. As an added benefit, this closure receives the Timer
object, so you can invalidate
it once done.
class ViewController: UIViewController {
private let slTimer = SLTimer()
override func viewDidLoad() {
super.viewDidLoad()
Timer.scheduledTimer(timeInterval: 0.3, repeats: true) { [weak self] timer in
guard let self, !self.slTimer.isDone else {
timer.invalidate()
return
}
self.setButtonTitle()
}
}
func setButtonTitle() {
saleButton.setTitle(self.slTimer.formattedTimeRemaining(), for: .normal)
}
}
For your SLTimer
class, use init
to set things that should only be set once and use DateComponentsFormatter
to simplify the formatting code.
I would separate the functions that tell you whether the timer is done and perform the formatting so that you can more easily see when the time is "done".
To know whether the time is up, you can use the timeIntervalSinceNow
property of your target Date
- This returns the difference (in fractional seconds) since the current date. If this value is <=0 then the date is in the past and your time is up.
class SLTimer {
private var endDate: Date
private var formatter: DateComponentsFormatter
init() {
self.formatter = DateComponentsFormatter()
self.formatter.unitsStyle = .positional
self.formatter.allowedUnits = [.hour, .minute, .second]
self.formatter.zeroFormattingBehavior = .pad
self.endDate = UserDefaults.standard.value(forKey: "waitingDate") as? Date ?? Calendar.current.date(byAdding: .second, value: 35, to: Date())!
UserDefaults.standard.set(self.endDate, forKey: "waitingDate")
}
var isDone: Bool {
return self.endDate.timeIntervalSinceNow <= 0
}
func formattedTimeRemaining() -> String {
guard !self.isDone else {
return "stop"
}
return self.formatter.string(from: Date(), to: self.endDate) ?? ""
}
}