【Swift・SwiftUI】端末の設定によらず、正しい Date を NTP から取得する

2021/03/03に公開

Summary

  • NTP から日時を取得し、Swift の Date として得る
  • Lyft の MobileNativeFoundation/Kronos を使う
  • iOS(Mac Catalyst を含む)、macOS、tvOS で使える

端末の設定から得られる Date

Swift では日時を扱いたい場合に Date を使います。

let date = Date()

この date に入るのは、その時点で端末に設定されている日時となります。多くの場合はこれで済みますが、端末の設定で日時を自由に設定できるため、あくまで正しい日時を取得したいとなった場合は別な方法を取らなければなりません。

外部から正しい Date を得る

端末から日時を取得しないということにすると、外部のネットワークへの接続が必須となり、その中で次のような方法が考えられます。

  • バックエンドのサーバー、mBaaS 等から得る(サーバーに設定されている日時が正しいと仮定)
  • 公開されている NTP サーバーから得る

バックエンドのサーバーを設置するほどでもない小規模なプロジェクト等の場合、この正しい日時を得るためだけにサーバーを設置するのは大変です。今回は公開されている NTP サーバーから日時情報を得ることとします。

NTP サーバーから正しい Date を得る

Swift で NTP サーバーから日時を得るための OSS ライブラリはいくつか存在しています。

https://github.com/MobileNativeFoundation/Kronos

https://github.com/instacart/TrueTime.swift

本記事では MobileNativeFoundation/Kronos を用いた例を紹介します。メインのブランチで Swift Package Manager に対応済みであり、Lyft によってメンテナンスされています。これの Android 版、lyft/Kronos-Android も存在します。

MobileNativeFoundation/Kronos は iOS(Mac Catalyst を含む)、macOS、tvOS で利用できます。watchOS や Linux 環境では利用できません。

Kronos を使って正しい Date を得る

Clock を同期する

Clocksync(from:samples:first:completion:) で NTP サーバーの日時と同期します。

Clock.sync()

私の場合、NICT の公開 NTP サービス を利用したいので、そのように指定します。上のように指定がない場合はデフォルトで "time.apple.com" が使用されます。

Clock.sync(from: "ntp.nict.jp")

Clock から Date を得る

Clock の同期が正しく行われると、Clock.now から同期した NTP サーバーの日時情報を元にした Date を得ることができます。

Clock.now // 同期前のため nil
// ...
Clock.sync()
// ...
Clock.now // `Clock.sync()` が完了した後のため `Date?`

iOS、macOS、tvOS での実行結果

このように、NTP サーバーの日時情報から Date を得ることができました🎉

SwiftUI によるサンプルコード

上記のスクリーンショットを撮影するために作成した SwiftUI によるプロジェクトのコードを示します。
iOS(Mac Catalyst を含む)、macOS、tvOS で共通のコードですが、macOS の場合は TARGET の Signing & Capabilities にある「App Sandbox」の Network - Outgoing Connections (Client) をオンにする必要があります。

DateFromNTPApp.swift と ContentView.swift
import SwiftUI
import Kronos

@main
struct DateFromNTPApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView().environmentObject(ContentViewHolder())
        }
    }
}

class ContentViewHolder: ObservableObject {
    private var timer: Timer!
    private let formatter = DateFormatter()
    @Published var timeStr = "時間を表示します…"
    
    init() {
        Clock.sync(from: "ntp.nict.jp")
        
        self.formatter.locale = Locale(identifier: "ja_JP")
        self.formatter.dateStyle = .medium
        self.formatter.timeStyle = .medium
        
        self.timer?.invalidate()
        self.timer = Timer.scheduledTimer(withTimeInterval: 1/60, repeats: true) { _ in
            if let date = Clock.now {
                self.timeStr = self.formatter.string(from: date)
            } else {
                self.timeStr = "NTP との同期を待っています…"
            }
        }
    }
}

struct ContentView: View {
    @EnvironmentObject var holder: ContentViewHolder
    
    var body: some View {
        Text(holder.timeStr)
            // 数字のみ等幅なフォントを使用する
            .font(Font.body.monospacedDigit())
            .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

記事執筆のきっかけ

端末の設定によらずに正しい日時を取得したい場合、これまで私は NICT の「http/httpsを利用した時刻配信」を利用した自作の Swift コードを使用していました。しかし、NICT によってネットワークを利用した時刻配信におけるNTPへの一元化への取り組みが開始されており、NTP を用いる方式に変更する必要が出てきたため、MobileNativeFoundation/Kronos を使うこととし、この記事が完成しました。

Discussion