【Swift・SwiftUI】端末の設定によらず、正しい Date を NTP から取得する
Summary
- NTP から日時を取得し、Swift の
Date
として得る - Lyft の MobileNativeFoundation/Kronos を使う
- iOS(Mac Catalyst を含む)、macOS、tvOS で使える
Date
端末の設定から得られる Swift では日時を扱いたい場合に Date
を使います。
let date = Date()
この date
に入るのは、その時点で端末に設定されている日時となります。多くの場合はこれで済みますが、端末の設定で日時を自由に設定できるため、あくまで正しい日時を取得したいとなった場合は別な方法を取らなければなりません。
Date
を得る
外部から正しい 端末から日時を取得しないということにすると、外部のネットワークへの接続が必須となり、その中で次のような方法が考えられます。
- バックエンドのサーバー、mBaaS 等から得る(サーバーに設定されている日時が正しいと仮定)
- 公開されている NTP サーバーから得る
バックエンドのサーバーを設置するほどでもない小規模なプロジェクト等の場合、この正しい日時を得るためだけにサーバーを設置するのは大変です。今回は公開されている NTP サーバーから日時情報を得ることとします。
Date
を得る
NTP サーバーから正しい Swift で NTP サーバーから日時を得るための OSS ライブラリはいくつか存在しています。
- MobileNativeFoundation/Kronos: Elegant NTP date library in Swift
- instacart/TrueTime.swift: NTP library for Swift and Objective-C. Get the true time impervious to device clock changes.
本記事では MobileNativeFoundation/Kronos を用いた例を紹介します。メインのブランチで Swift Package Manager に対応済みであり、Lyft によってメンテナンスされています。これの Android 版、lyft/Kronos-Android も存在します。
MobileNativeFoundation/Kronos は iOS(Mac Catalyst を含む)、macOS、tvOS で利用できます。watchOS や Linux 環境では利用できません。
Kronos を使って正しい Date
を得る
Clock
を同期する
Clock
の sync(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) をオンにする必要があります。
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