🕰️

【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