[Swift] 1秒を正確に刻む

# Swiftのインターバルタイマーの精度は高い

毎秒インベントを欲しい場合、次のAPIを使用すると思いますが、どちらのAPIも、正確にタイマー処理が呼ばれることが分かりました。その精度は誤差ほぼ1ミリ秒未満です。

```swift
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: timerEvent)
// OR
Timer.publish(every: 1.0, on: .main, in: .common)
```

精度は、タイマー処理内で 現在時刻と秒未満のナノ秒を求めることで分かります。
秒未満のナノ秒は、$10^9$で割って秒に変換します。

```swift
let nanosecond = Calendar.current.component(.nanosecond, from: Date.now)
let lessThanSeconds = Double(nanosecond) / 1e9
```

`正確に1秒を刻む`ということは、この秒未満の値が、毎回、ほぼ`同じ値`であることを意味します。
以下のコードで確認できます。

```swift:for Playground
import Foundation
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { _ in
    let now = Date.now
    let nanosecond = Calendar.current.component(.nanosecond, from: now)
    let lessThanSeconds = Double(nanosecond) / 1e9
    print(now.formatted(date: .omitted, time: .standard), lessThanSeconds)
})
//RunLoop.current.run()
```

```swift:for SwiftUI（平均値あり）
import SwiftUI

struct ContentView: View {
    let timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()

    @State var currentTime = Date.now
    @State var lessThanSeconds = 0.0
    @State var avarage = 0.0
    @State var count = 0
    @State var sum = 0.0

    var body: some View {
        VStack {
            HStack {
                Text("現在時刻: ")
                Text(currentTime.formatted(date: .omitted, time: .standard))
            }
            HStack {
                Text("秒未満: ")
                Text("\(lessThanSeconds)")
            }
            HStack {
                Text("平均: ")
                Text("\(avarage)")
            }
            HStack {
                Text("平均差: ")
                Text("\(abs(lessThanSeconds - avarage))")
            }
        }
        .frame(width: 200)
        .padding()
        .onReceive(timer) { _ in
            currentTime = Date.now
            lessThanSeconds = Double(Calendar.current.component(.nanosecond, from: currentTime)) / 1e9
            sum += lessThanSeconds; count += 1
            avarage = sum / Double(count)            
        }
    }
}
```

毎秒、秒未満の値の平均値を取ると、毎回の差がほぼ1ミリ秒しか無いことが分かります。以外なほど正確です。正直、これほど精度が高いとは思っていませんでした。


# "every hour" & "on the hour" の "秒"版 

実は、ここからが、この記事の本当の目的です。

（"on the hour"）"正時"とは、1時0分0秒, 2時0分0秒, 3時0分0秒 の様に、"分"と"秒"がちょうど 0 の時刻を指しますが、これの"秒"版、a時b分0.000秒、a時b分1.000秒、a時b分2.000秒、a時b分3.000秒、・・・の様に、"ちょうどの秒"にイベントが欲しい場合を考えます。
（ここでは、"正時"ならぬ"正秒"と呼ぶことにします）

前項で説明した通り、1秒（毎秒）はほぼ正確に刻むことが分かりましたので、このタイマーイベントの開始を限りなく、 x.000秒に起動できればよい ということになります。

そこで、次の処理を考えました。

> _現在時刻(a)を秒未満のナノ秒まで求め、"次の秒"の開始(b)までの残り時間を求める。その時間だけ待ってからタイマーを起動する。_


- (a) hh：mm：ss.zzzzzz
- (b) hh：mm：(ss + 1).000000
- (c) "次の秒"の開始までの残り時間 ＝ (b)-(a)

以下のコードで実装できます。

```swift
let nowInterval = Date.now.timeIntervalSinceReferenceDate //(a)
let nextSecond = floor(nowInterval + 1) //(b)
let remainSecond = nextSecond - nowInterval //(c)
Thread.sleep(forTimeInterval: remainSecond) //wait
//start timer （(c)秒待てタイマーを起動する）
let currentTime = Date.now
let lessThanSeconds = Double(Calendar.current.component(.nanosecond, from: currentTime)) / 1e9
print(currentTime.formatted(date: .omitted, time: .standard), lessThanSeconds)
```

このコードを何回か実行してみると、最後のprintの結果は、毎回ほぼ5ミリ秒以内の値となります。つまり、毎回（毎秒） `hh:mm:ss.005xxx`という結果であり、ピッタリ`x.000`秒とはいきませんが、`x.005`秒（5ミリ秒）程度なら十分な精度だと思います。

前出のSwiftUIのコードに組み込みます。

```swift:SwiftUI
import Combine

struct ContentView: View {
    //let timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
    let timer = startTimer()

 　（中略）

    private static func startTimer() -> Publishers.Autoconnect<Timer.TimerPublisher> {
        let offset = 0.18
        let nowInterval = Date.now.timeIntervalSinceReferenceDate
        let nextSecond = floor(nowInterval + 1)
        var remainSecond = nextSecond - nowInterval
        if remainSecond > offset { remainSecond -= offset }
        Thread.sleep(forTimeInterval: remainSecond)
        let timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
        //print(nowInterval, remainSecond)
        return timer
    }
}
```

`Timer.publish(every:on:in:).autoconnect()`のオーバーヘッドが大きいためか、200ミリ秒以上の誤差（`x.200`秒）となっていました。プレビューだと、30ミリ秒程度の誤差（`x.030`秒）。
そこで、オーバーヘッド分として0.18秒を加味するようにoffsetを定義してみました。

**例えば、時計アプリを作成する場合、1秒間に何回も描画することは無駄であるため、今回の"正秒"イベント処理を使い、数十ミリ秒程度の精度を期待したい（毎秒x.030秒に描画する）。
上記のコードでは、オフセット0.18秒とすることで、ほぼこの精度となった。**

> いや 待てよ、このオフセット値を自動計算できないか？

ダミーのタイマーイベントを開始して、イベント発生までの遅延時間を実測することで、このオフセット値を自動計算できるはずだ。

# オーバーヘッドの自動計算

`Timer.publish(every:on:in:).autoconnect()`のオーバーヘッドを計測するため、ダミーのタイマーイベントを起動し、最初のイベントが起動されたときの時刻の差からオーバーヘッドを計算します。
そして次に、"次の秒"の開始までの残り時間に、求めたオーバーヘッドを加味した時間だけ待って、本物のインターバルタイマを起動します。

```swift
import SwiftUI

struct ContentView: View {
    enum TimerMode {
        case dummy(Date) //遅延時間を計測するためのダミーイベント
        case genuine //本物のタイマーイベント
        case initial //（初期値）これもダミー
    }

    @State var timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
    @State var timerMode = TimerMode.initial
    @State var currentTime = Date.now
    @State var lessThanSeconds = 0.0
    @State var avarage = 0.0
    @State var count = 0
    @State var sum = 0.0

    var body: some View {
        VStack {
            HStack {
                Text("現在時刻: ")
                Text(currentTime.formatted(date: .omitted, time: .standard))
            }
            HStack {
                Text("秒未満: ")
                Text("\(lessThanSeconds)")
            }
            HStack {
                Text("平均差: ")
                Text("\(abs(lessThanSeconds - avarage))")
            }
        }
        .frame(width: 200)
        .padding()
        .onAppear {
            //ダミーイベントを起動
            timerMode = .dummy(Date.now)
            timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
        }
        .onReceive(timer) { _ in
            switch timerMode {
                case .initial: timer.upstream.connect().cancel() //捨て
                case .dummy(let startDateTime):
                    timer.upstream.connect().cancel()
                    let delaySeconds = -startDateTime.timeIntervalSinceNow - 1.0 //オーバーヘッド実測値
                    
                    let nowInterval = Date.now.timeIntervalSinceReferenceDate
                    let nextSecond = floor(nowInterval + 1) //"次の秒"
                    let remainSecond = nextSecond - nowInterval - delaySeconds //オーバーヘッドを加味
                    Thread.sleep(forTimeInterval: remainSecond)

                    //本物のタイマーイベントを起動
                    timerMode = .genuine
                    timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
                    //print(Date.now.formatted(date: .omitted, time: .standard), delaySeconds)
                    
                case .genuine:
                    //本物のタイマーイベント
                    currentTime = Date.now
                    lessThanSeconds = Double(Calendar.current.component(.nanosecond, from: currentTime)) / 1e9
                    sum += lessThanSeconds; count += 1
                    avarage = sum / Double(count)
                    //print(currentTime.formatted(date: .omitted, time: .standard), lessThanSeconds)
            }
        }
    }
}
```

この結果、数ミリ秒〜20ミリ秒程度の範囲となり、期待する精度に収めることができました。（ただし、逆にプレビューの場合の精度は悪くなりました。）

以下は、上記コードの実行結果の動画。


<iframe width="274" height="200" src="https://www.youtube.com/embed/p_Ws2BeNJbo?autoplay=1&mute=1&loop=1&playlist=p_Ws2BeNJbo" title="&quot;正時&quot;ならぬ&quot;正秒&quot;にイベント起動" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

_画面録画したタイミングは、何故か、1ミリ秒未満と特に精度が高かった。_

> プレビューやPlaygroundだと、何故かこの精度にはなりません。
ビルドしたMacAppを録画しました。iOSアプリでもOK。


（上記コードでは省略したが、記録した動画においては、["数字"のみ等幅フォント](https://qiita.com/nak435/items/bc79b6eeea9091b646e5)を使用している。）



# おわりに

今回の試みのきっかけは、偶然 目にした「Swiftで単純な時計を作る」記事が0.1秒のインターバルタイマを使っていたことである。

PCが秒を刻む中、1秒のインターバルタイマだと、最大で限りなく1秒遅れの表示となる可能性があるため、1秒間に10回描画すれば安全だという発想である。確かにそうすれば、最大でも0.1秒遅れの表示となり、人間の感覚であれば、ほぼリアルタイムであると言ってもよいとは思うが、CPUリソースを無駄に消費するので、エコでは無い。

1秒のインターバルタイマを使い、かつ、"正秒"にイベントが発生するなら、表示遅れの問題も解消される。そんな発想から考えた内容である。

なお、説明が下手で、うまく伝わるか不安が残るが、何かの参考になれば幸いです。

**以上**