Search code examples
swifttimercalendaruikit

How to create a 24 hour timer?


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"
}

Solution

  • 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 Timers 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) ?? ""
        }
    }