swiftdatetimerbreakpointsrunloop

Bug with Timer and/or RunLoop?


I had tested this extensively in the past before starting the project I'm working on. I copied and pasted the code into my new project, so it should work. All I want it to do is reload the tableView every minute, so that the clocks displayed on the tableView reload.

func setMinutesForReload(){
    
    //get calender object
    let calender = Calendar.current
    //curent date
    let now = Date()
    //create an array of dates to store minutes
    var minutes = [Date]()
    //timer array to store timers
    var timers = [Timer]()
    
    //set a run loop
    for i in 0...23{
        for j in 0...59{
            minutes.append(calender.date(bySettingHour: i, minute: j, second: 0, of: now)!)
            timers.append(Timer(fireAt: minutes[j], interval: 0, target: self, selector: #selector(minutelyReloadTimer), userInfo: nil, repeats: false))
            RunLoop.main.add(timers[j], forMode: RunLoop.Mode.common)
        }
    }
    
} 

@objc func minutelyReloadTimer(){
    self.cityTableView.reloadData()
}

Oddly enough, when I run the app and set a breakpoint, I see that it calls minutelyReloadTimer() immediately, 59 times in a row (I counted).

enter image description here

Is this an issue with a recent Xcode/Swift update, or am I missing something that I'm not seeing?


Solution

  • I'm certain the unexpected behavior you are witnessing is not due to an issue with Xcode/Swift.

    Taking a look at your code, I noticed a couple strange things right off the bat. For one, RunLoop.main.add(timers[j]... will only add timer's 0-59 to the run loop, over and over (24 times to be exact). And for two, calender.date(bySettingHour: i, minute: j, second: 0, of: now)'s default configuration is to find the next time that matches the given data components, since the function's last parameter (i.e. matchingPolicy: Calendar.MatchingPolicy) is assigned a default value of .nextTime. Therefore, the dates that will be returned are for tomorrow, not today.

    Now, instead of dissecting your (unnecessarily complicated) method further, I would suggest a much simpler approach: fire a repeating 60 second timer, with the initial fire being on the start of the next minute.

    In short, refactoring your code would result in the following.

    func setMinutesForReload(){
        
        //get calender object
        let calender = Calendar.current
        //curent date
        let now = Date()
        
        let dateOfNextMinute: Date = calender.nextDate(after: now, matching: DateComponents(second: 0), matchingPolicy: .nextTime)!
        let timer = Timer(fireAt: dateOfNextMinute, interval: 60.0, target: self, selector: #selector(minutelyReloadTimer), userInfo: nil, repeats: true)
        RunLoop.main.add(timer, forMode: RunLoop.Mode.common)
    }
    
    @objc func minutelyReloadTimer(){
        print("Reload")
    }