如何在使用定时器更新时间时,使得时间与macOS系统时钟同步?

3

我正在使用这段代码在 NSLabel 中显示时间:

模型

func startClock() {
    timer = Timer(timeInterval: 1.0, target: self, selector: #selector(fireAction), userInfo: nil, repeats: true)
    RunLoop.current.add(timer!, forMode: RunLoop.Mode.common)
    timer?.tolerance = 0.05
    fireAction()
}

@objc dynamic func fireAction() {
    delegate?.timeToUpdateClock(self)
}

视图控制器

extension ViewController: ClockWorksProtocol {
    func timeToUpdateClock(_ timer: ClockWorks) {
        tick()
     }
}

 func tick() {
    clockDateLabel.stringValue = DateFormatter.localizedString(from: Date(), dateStyle: .full, timeStyle: .none)
    clockTimeLabel.stringValue = DateFormatter.localizedString(from: Date(), dateStyle: .none, timeStyle: .medium)
}

一切似乎都很正常,但不幸的是显示的秒数与菜单栏上时钟所看到的秒数不同步。我认为这是因为我每秒更新一次时钟,但用户没有在正确的时间点击开始,而是在一个随意的间隔内从一秒到下一秒(我使用1.0的计时器时间间隔)。
我可以将计时器的时间间隔从1.0秒更改为0.1秒。这似乎有效,但我担心这会消耗过多的资源。
我发布了两张同时拍摄的屏幕截图。它们显示不同的时间(秒数)。

enter image description here

enter image description here

我该如何解决这个间隙?


什么意义上不同步?Date()是时钟上的时间。它就是现在,简单明了。 - matt
我是Matt,我编辑了问题以使其更清晰。谢谢。 - Cue
3
不要盲目重复使用计时器,查看当前时间并设置一个间隔,在下一秒开始时触发一次。 - matt
嗨,马特,感谢您的有用评论,您能否好心创建一个答案,以便我可以接受它?如果您还可以添加一个示例来澄清您建议的技术,那就更好了。 - Cue
2个回答

2
我知道有两种方法可以在这里采取:
  1. 计算现在和下一个整秒之间的时间 dt
let nextSecond = floor(now.timeIntervalSince1970) + 1
let dt = nextSecond - now.timeIntervalSince1970

创建一个定时器,它将在 dt 时间后精确触发一次。在该定时器的回调函数中,启动一个每秒执行一次 tick() 的定时器,这正是你的 startClock() 函数所做的。
如果你想要尽快开始计时,也可以在第一个定时器触发时运行 tick()
  1. 确定下一秒会发生的时间点作为日期。

Date(timeIntervalSince1970: floor(Date().timeIntervalSince1970) + 1)

使用此初始化方法为定时器设置特定的 fireAt 日期,并将其设置为每秒重复一次。

let timer = Timer(fireAt: 
                      Date(timeIntervalSince1970:
                               floor(Date().timeIntervalSince1970) + 1),
                  interval: 1.0,
                  target: self,
                  selector: #selector(fireAction),
                  userInfo: nil, 
                  repeats: true)

1

我有一个应用程序也遇到了同样的问题。不幸的是,关于这个问题的信息在互联网上非常罕见。因此,我希望我的解决方案能够帮助其他人。

首先,确定与下一秒之间的时间差:

let now = Date()
let nextSecond = now.timeIntervalSince1970.rounded(.down) + 1
let intervalToWait = nextSecond - now.timeIntervalSince1970

现在,您可以使用此计算出的时间间隔几乎精确地启动计时器到下一个完整秒。然后计时器只需要一个秒的间隔就可以与系统时钟同步工作。为什么我的答案中缺少了这个初始化代码呢?简单:不要使用标准计时器。

标准计时器类 不适用于此用例。许多边角情况证明了这一点。其中之一是当 Mac 进入睡眠模式时。根据 Mac 醒来的毫秒数,时间再次与系统时钟不同步。看看这个虚构的例子:

tick() => 12:00:00:00
tick() => 12:00:01:00
tick() => 12:00:02:00
tick() => 12:00:03:00
sleep mode => 12:00:03:60
wake up => 12:00:07:30
tick() => 12:00:07:30
tick() => 12:00:08:30
tick() => 12:00:09:30

因此,我们需要一个定时器,它不会在每次激活/重新激活时盲目地累加一秒钟,而是会与实际时间同步进行。这可以通过DispatchSourceTimerDispatchWallTime来实现:

private var timer: DispatchSourceTimer!

// ...

self.timer = DispatchSource.makeTimerSource(flags: .strict, queue: DispatchQueue.global(qos: .userInteractive))
self.timer.schedule(wallDeadline: .now() + intervalToWait, repeating: 1.0, leeway: .nanoseconds(0))
self.timer.setEventHandler(qos: .userInteractive, flags: .enforceQoS) {
    DispatchQueue.main.async {
        self.tick()
    }
}
self.timer.resume()

到目前为止,我的用户没有报告任何与此解决方案相关的问题。因此,我认为实现是正确的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接