01 何が問題だったのか
SE-0329 で導入された Clock の枠組みでは、標準ライブラリ側に ContinuousClock と SuspendingClock が追加されました。これらはストップウォッチ的な計測には向きますが、いずれもシステム内部の基準点からの経過時間を扱う clock であり、「ユーザーが普段目にする時計」、いわゆる wall clock として使うことを意図したものではありません。
一方で実際のアプリケーションでは、ユーザーの暮らしのリズムに合わせて何かを実行したい場面が数多くあります。たとえば「明日の朝 8 時にアラームを鳴らす」「日付が変わったタイミングでメンテナンスを走らせる」といった要件は、システムのタイムゾーン設定やサマータイムの切り替え、ロケールに応じたカレンダーといった概念を抜きには扱えません。ContinuousClock や SuspendingClock の Instant は単なる経過時間しか持たないので、こうしたカレンダーや時刻表示と直接つなぐ用途には適していません。
カレンダー計算と直接結び付けて時刻を扱う型としては Foundation に従来から Date がありました。しかし Date 自体は SE-0329 の Clock / InstantProtocol の枠組みには組み込まれておらず、Date を使ったスケジューリングを clock.sleep(until:) の形で表現する標準的な手段がありませんでした。また、SE-0329 の「将来の見通し」では UTC をベースとした clock の追加が示唆されていましたが、API としては未提供のままでした。
加えて、Date は歴史的・天文学的な用途で必要となる leap second(うるう秒)を、それ単体や Calendar 経由でも扱えませんでした。普段のアプリケーションでは leap second を意識しないで済みますが、過去の特定の日付間で正確な経過秒数を求めたい一部のユースケースでは、Foundation 側に計算手段がないために自前で対応する必要がありました。
最後に、SE-0473 で ContinuousClock と SuspendingClock にエポックを表すプロパティが追加されたことで、clock のエポックを取り出せる API があると便利な場面があるという認識が共有されました。UTCClock を新設するにあたっては、これら 2 つの clock とそろえる形でエポックを露出する API も求められていました。
02 どのように解決されるのか
Foundation に新しい clock として UTCClock が追加され、Date が InstantProtocol に適合します。あわせて、各 clock のエポックを取得するためのプロパティが導入されます。
UTCClock
UTCClock は UTC を基準とする時刻で進む clock で、Instant 型は Date です。状態を持たないため Sendable で、デフォルトイニシャライザだけで使えます。
@available(FoundationPreview 6.2, *)
public struct UTCClock: Sendable {
public typealias Instant = Date
public init()
}
@available(FoundationPreview 6.2, *)
extension UTCClock: Clock {
public func sleep(until deadline: Date, tolerance: Duration? = nil) async throws
public var now: Date { get }
public var minimumResolution: Duration { get }
}
Date をそのまま Instant として扱えるので、Calendar を使って組み立てた具体的な日時まで clock.sleep(until:) で待つ、というスケジューリングを自然に書けます。タイムゾーンやサマータイム、ロケールに応じた挙動も、Calendar の側で吸収されます。
let calendar = Calendar.current
var when = calendar.dateComponents([.day, .month, .year], from: .now)
when.day = when.day.map { $0 + 1 }
when.hour = 8
if let tomorrowMorning8AM = calendar.date(from: when) {
try await UTCClock().sleep(until: tomorrowMorning8AM)
playAlarmSound()
}
このように UTCClock は、アラーム、定時実行のメンテナンス、ユーザーの生活時間に合わせたバックグラウンド処理など、wall clock 的なスケジューリングを必要とするシナリオで使うことを想定しています。一方で、計測対象の時間が必ず単調に進むことが要求される処理では引き続き ContinuousClock や SuspendingClock を使うべきで、UTCClock はそれらの置き換えではありません。
minimumResolution は Date の最小粒度に対応します。一般には 1 ナノ秒程度ですが、Foundation を実装するプラットフォームによって異なる場合があります。
Date の InstantProtocol 適合
Date が InstantProtocol に適合し、advanced(by:) と duration(to:) を備えるようになります。これにより Date 同士の差や、Date への Duration の加算が共通の API で扱えます。
@available(FoundationPreview 6.2, *)
extension Date: InstantProtocol {
public func advanced(by duration: Duration) -> Date
public func duration(to other: Date) -> Duration
}
加えて、2 つの Date の間に挟まれた leap second の合計を返す static メソッドが用意されます。
@available(FoundationPreview 6.2, *)
extension Date {
public static func leapSeconds(from start: Date, to end: Date) -> Duration
}
通常の経過時間計算では duration(to:) をそのまま使えば十分で、leap second を意識する必要はありません。歴史的・天文学的なデータセットなど、leap second まで含めた厳密な秒数が必要な場面に限って leapSeconds(from:to:) を併用します。
let start = Calendar.current.date(from: DateComponents(timeZone: .gmt, year: 1971, month: 1, day: 1))!
let end = Calendar.current.date(from: DateComponents(timeZone: .gmt, year: 2017, month: 1, day: 1))!
let leaps = Date.leapSeconds(from: start, to: end)
print(leaps) // 27.0 seconds
print(start.duration(to: end) + leaps) // 1451692827.0 seconds
duration(to:) のオーバーロードに includingLeapSeconds のような引数を加える案も検討されましたが、混同による誤用を避けるため、leap second を扱う API は別の static メソッドとして独立させる形になっています。
UTCClock のエポック
SE-0473 で ContinuousClock と SuspendingClock に追加された systemEpoch と同じ流れで、UTCClock にも systemEpoch プロパティが追加されます。UTCClock のエポックは Date(timeIntervalSinceReferenceDate: 0)、すなわち 2001 年 1 月 1 日 (UTC) に固定されています。
@available(FoundationPreview 6.2, *)
extension UTCClock {
public static var systemEpoch: Date { get }
}
このエポックは、UTCClock の Instant である Date の自然なゼロ点に対応します。InstantProtocol に適合した独自型を作る際にも、エポックに相当する基準点があるなら同じ規約で static プロパティとして公開しておくと、SE-0473 で導入された各 clock のエポックと一貫した形で扱えます。