【Swift・SwiftUI】端末の設定に依らずに NTP(apple/swift-ntp)を使って現実世界の現在日時を算出する
Summary
import Foundation
import NTPClient
let config = NTPClient.Config(version: .v4)
let ntp = NTPClient(
config: config,
server: "time.apple.com"
)
let response = try await ntp.query(timeout: .seconds(10))
let (seconds, attoseconds) = response.offset.components
let offsetTimeInterval = TimeInterval(seconds) + TimeInterval(attoseconds) * 1e-18
// NTP サーバーからの結果を元に補正された現在日時
let correctedDate = Date.now + offsetTimeInterval
apple/swift-ntp
過去に MobileNativeFoundation/Kronos を使って、NTP から現在日時を取得する方法を紹介しました。
これは記事執筆当時、iOS(Mac Catalyst を含む)・macOS・tvOS で利用できるライブラリでした。
そして 2025年5月、apple/swift-ntp という Swift 製の NTP クライアントが OSS として公開されました。こちらは Swift を利用できるすべての環境(iOS・macOS・tvOS・visionOS・watchOS・Linux・Windows・Android・...etc.(本記事執筆時点では Wasm を除く))で利用できます。今回はこれを用いて端末の設定に依らない現在日時の算出を行います。
Date
は決まる
端末の設定から Swift では日時を取り扱う際に Date
を用います。
import Foundation
// 端末の設定による現在日時
let date = Date.now
この date
には、その時点で端末に設定されている日時が入ります。ユーザーは端末の設定で日時を自由に変更できるため、現実世界の日時と異なる値となっていることが考えられます。
外部から現実世界の現在日時を得る
端末に設定されている日時を信用できないような状況の場合、外部のネットワークからの取得を考えます。たとえば
- バックエンドのサーバー、mBaaS 等から得る
- サーバーに設定されている日時が正しいものであると仮定
- 公開されている NTP サーバーから得る
バックエンドのサーバーを設置するほどでもないようなプロジェクトの場合、すでに公開されている NTP サーバーから日時に関する情報を得るのが簡便でしょう。
NTP サーバーからの結果を元に現在日時を算出する
Apple から Swift 製の NTP クライアントライブラリ、apple/swift-ntp が公開されており、これを用います。
Package.swift で apple/swift-ntp を依存関係に追加する
let package = Package(
name: "NTPPlayground",
dependencies: [
.package(url: "https://github.com/apple/swift-ntp", exact: "0.3.0"),
],
targets: [
.target(
name: "NTPPlayground",
dependencies: [
.product(name: "NTPClient", package: "swift-ntp"),
]
),
],
swiftLanguageModes: [.v6]
)
Xcode(Xcode プロジェクトの Package Dependencies)で apple/swift-ntp を依存関係に追加する
https://github.com/apple/swift-ntp
クエリを送りオフセットを得る
NTP サーバーにクエリを送り、端末に設定されている日時とのオフセットを得ます。NTP サーバーとして time.apple.com
を使う例です。
import Foundation
import NTPClient
let config = NTPClient.Config(version: .v4)
let ntp = NTPClient(
config: config,
server: "time.apple.com"
)
let response = try await ntp.query(timeout: .seconds(10))
let (seconds, attoseconds) = response.offset.components
let offsetTimeInterval = TimeInterval(seconds) + TimeInterval(attoseconds) * 1e-18
日本標準時ベースで提供されている NICT の公開 NTP サービス を利用する場合は、ntp.nict.jp
を指定します。
server: "ntp.nict.jp"
オフセットを使って現実世界の日時を算出する
オフセットを用いて現実世界の日時を Date
にします。
let now = Date.now
let deviceDate = now // 端末に設定されている現在日時
let correctedDate = now + offsetTimeInterval // NTP サーバーからの結果を元に補正された現在日時
これで現実世界の日時を Date
として得ることができました🎉
SwiftUI によるサンプルコード
上記のスクリーンショットを撮影するために作成した SwiftUI によるプロジェクトのコードを示します。iOS・macOS・tvOS・visionOS・watchOS で共通のコードですが、macOS の場合は TARGET の Signing & Capabilities にある「App Sandbox」の Network - Outgoing Connections (Client) をオンにする必要があります。
macOS の Signing & Capabilities の設定
import NTPClient
import SwiftUI
struct ContentView: View {
@State private var deviceDate: Date = .now
@State private var correctedDate: Date? = nil
@State private var ntpError: (any Error)? = nil
var body: some View {
VStack {
LabeledContent(
"Device Time",
value: deviceDate,
format: Date.FormatStyle(date: .abbreviated, time: .complete)
)
.monospacedDigit()
LabeledContent("Corrected Time") {
if let correctedDate {
Text(
correctedDate,
format: Date.FormatStyle(date: .abbreviated, time: .complete)
)
} else {
Text(ntpError?.localizedDescription ?? "Loading...")
}
}
.monospacedDigit()
}
.padding()
.task {
do {
try await autoupdatingDate()
} catch {
ntpError = error
}
}
}
func autoupdatingDate() async throws {
let config = NTPClient.Config(version: .v4)
let ntp = NTPClient(
config: config,
server: "time.apple.com"
)
let response = try await ntp.query(timeout: .seconds(10))
let (seconds, attoseconds) = response.offset.components
let offsetTimeInterval = TimeInterval(seconds) + TimeInterval(attoseconds) * 1e-18
while true {
let now = Date.now
deviceDate = now
correctedDate = now + offsetTimeInterval
try await Task.sleep(for: .seconds(1 / 30)) // 約 30 Hz で表示を更新
}
}
}
Discussion