user7763839
user7763839

Reputation:

reset date to next hour after top of hour is reached

I Want My swift code to count down to the nearest top of the hour. So if the time is 146 the user code should count down 14 minutes. Right now My code below counts down to a spefic day and time. I just want it to count down to the nearest hour when the app is running.

import UIKit

class ViewController: UIViewController {
    @IBOutlet var timerLabel: UILabel!
    
    var timer: Timer!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(UpdateTime), userInfo: nil, repeats: true)
    }
    
    @objc func UpdateTime() {
        let userCalendar = Calendar.current
        // Set Current Date
        let date = Date()
        let components = userCalendar.dateComponents([.hour, .minute, .month, .year, .day], from: date)
        let currentDate = userCalendar.date(from: components)!
        
        // Set Event Date
        var eventDateComponents = DateComponents()
        eventDateComponents.year = 2021
        eventDateComponents.month = 01
        eventDateComponents.day = 01
        eventDateComponents.hour = 01
        eventDateComponents.minute = 00

        eventDateComponents.timeZone = TimeZone(abbreviation: "GMT")
        

        let eventDate = userCalendar.date(from: eventDateComponents)!
        
        let timeLeft = userCalendar.dateComponents([.day, .hour, .minute, ], from: currentDate, to: eventDate)
    
        timerLabel.text = "\(timeLeft.day!)d \(timeLeft.hour!)h \(timeLeft.minute!)m "

        endEvent(currentdate: currentDate, eventdate: eventDate)
    }
    
    func endEvent(currentdate: Date, eventdate: Date) {
        if currentdate >= eventdate {
            timerLabel.text = "Happy New Year!"
            // Stop Timer
            timer.invalidate()
        }
    }
}

edit/update:

My goal in my swift code is when the top of the hour is reached. After trying to implement @Leo's answer it prints "Top of Hour" and it does the problem is that It only does it one time. As long as the app is open I want it to print "Top of Hour" at every hour. So I need to reset the end date which is what I tried to do at

let date = Date() end = date.nextHour

That does not let the code compile. So I have to reset the end var to the next hour.

Upvotes: 0

Views: 202

Answers (1)

Leo Dabus
Leo Dabus

Reputation: 236305

No need to update the user interface 10 times per second. As it is it will drain the device's battery much faster than needed while it should only run once a minute. You can change your timer timeInterval to 1 second and schedule it to fire at the next even second. To get the next even hour and the next even minute you can use Calendar method

func nextDate(after date: Date, matching components: DateComponents, matchingPolicy: Calendar.MatchingPolicy, repeatedTimePolicy: Calendar.RepeatedTimePolicy = .first, direction: Calendar.SearchDirection = .forward) -> Date?

Just create two computed properties extending Date and pass zero for minute or nanosecond components:

extension Date {
    var nextHour: Date {
        Calendar.current.nextDate(after: self, matching: DateComponents(minute: 0), matchingPolicy: .strict)!
    }
    var nextSecond: Date {
        Calendar.current.nextDate(after: self, matching: DateComponents(nanosecond: 0), matchingPolicy: .strict)!
    }
    var minute: Int {
        Calendar.current.component(.minute, from: self)
    }
}

Now add a property to your view controller to keep a reference of the end date. Note that there is no need to declare your timer as optional:

var end: Date?
var timer = Timer()

And create a DateComponentsFormatter to create a localized description of the remaining time:

extension Formatter {
    static let minutesRemaining: DateComponentsFormatter = {
        let formatter = DateComponentsFormatter()
        formatter.formattingContext = .standalone
        formatter.unitsStyle = .short
        formatter.allowedUnits = [.minute, .second]
        formatter.includesTimeRemainingPhrase = true
        return formatter
    }()
}

Now you just setup the end date and to schedule your timer to fire at the next even minute:

override func viewDidLoad() {
    super.viewDidLoad()
    // get the current date
    let date = Date()
    // set the end date
    end = date.nextHour
    // schedule the timer to fire at the next even second and set its interval to 1 second
    timer = .init(fireAt: date.nextSecond, interval: 1, target: self, selector: #selector(updateUI), userInfo: nil, repeats: true)
    RunLoop.main.add(timer, forMode: .common)
    updateUI()
}
@objc func updateUI() {
    if Date().minute == 0 || Date() > end {
        end = Date().nextHour
        timerLabel.text = "beginning of hour"
        print("beginning of hour")
    } else {
        // update the remaining time (for a perfect sync we need to subtract a second from the current time)
        let text = Formatter.minutesRemaining.string(from: Date().addingTimeInterval(-1), to: end) ?? ""
        timerLabel.text = text
        print(text)
    }
}

Upvotes: 0

Related Questions