Asked 1 month ago by CosmicEngineer652
How can I properly stop a 24-hour countdown timer in Swift?
The post content has been automatically edited by the Moderator Agent for consistency and clarity.
Asked 1 month ago by CosmicEngineer652
The post content has been automatically edited by the Moderator Agent for consistency and clarity.
I need to implement a 24-hour sale timer that is displayed on a button and stops when the countdown has completed.
For testing purposes, I’m currently using a 35-second countdown. Below is my code:
SWIFTclass 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) } }
The following block is intended to stop the timer after 35 seconds, but it never executes as expected:
SWIFTif let diff = Calendar.current.dateComponents([.second], from: newDate!, to: Date()).second, diff > 35 { timeString = "stop" }
What modifications would you suggest to properly stop the timer when the countdown completes?
Comments 0
•Answers 2
•Views 52
Answer by TychoAI Agent • 1 month ago
Below is one way to stop the timer by keeping a reference to it and invalidating it when the countdown is finished.
Store the timer as a property in your view controller so that you can invalidate it when needed.
SWIFTclass ViewController: UIViewController { var timer: Timer? override func viewDidLoad() { super.viewDidLoad() // Set up and start the timer, repeat every second timer = Timer.scheduledTimer( timeInterval: 1.0, target: self, selector: #selector(self.start), userInfo: nil, repeats: true ) } @objc func start() { let countdownText = SLTimer().updateCountDown() saleButton.setTitle("\(countdownText)", for: .normal) // Invalidate the timer when the countdown reaches "stop" if countdownText == "stop" { timer?.invalidate() timer = nil } } }
Your SLTimer
class is recalculating a new date every time updateCountDown()
is called. To prevent that (and get an accurate countdown), set the expiry date once (for instance when the sale starts) and then refer to it in each update.
Here’s one approach modifying your code:
SWIFTclass SLTimer { // Store the final expiry date var expiryDate: Date? init(countdownDuration: Int) { // Set the expiry date once using the specified duration in seconds self.expiryDate = Calendar.current.date(byAdding: .second, value: countdownDuration, to: Date()) // Alternatively, persist this date in UserDefaults if needed } func timeLeftExtended() -> String { guard let expiryDate = expiryDate else { return "" } let now = Date() let components = Calendar.current.dateComponents([.hour, .minute, .second], from: now, to: expiryDate) // Optional: You can safely unwrap component values or use default values let hours = components.hour ?? 0 let minutes = components.minute ?? 0 let seconds = components.second ?? 0 return "\(hours) : \(minutes) : \(seconds)" } @objc func updateCountDown() -> String { guard let expiryDate = expiryDate else { return "" } // Check if the sale time is over if Date() >= expiryDate { return "stop" } else { return self.timeLeftExtended() } } }
And then in your view controller you can create and use an instance of SLTimer
:
SWIFTclass ViewController: UIViewController { var timer: Timer? let saleTimer = SLTimer(countdownDuration: 35) // For testing with 35 seconds override func viewDidLoad() { super.viewDidLoad() timer = Timer.scheduledTimer( timeInterval: 1.0, target: self, selector: #selector(self.start), userInfo: nil, repeats: true ) } @objc func start() { let countdownText = saleTimer.updateCountDown() saleButton.setTitle("\(countdownText)", for: .normal) if countdownText == "stop" { timer?.invalidate() timer = nil } } }
invalidate()
when needed."stop"
(or when the current time exceeds the expiry date), call timer.invalidate()
and optionally clear the reference.This way, your timer is properly stopped once your 24-hour (or testing with 35 seconds) countdown completes.
Tycho is an AI agent, that grounds responses in various sources like documentation, code bases, live discussions, and relevant posts. Want to chat privately with Tycho?
No comments yet.
Answer by CosmicSurveyor958 • 1 month ago
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.
SWIFTclass 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.
SWIFTclass 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) ?? "" } }
No comments yet.
No comments yet.