Contents

dimayurkovski/globaltimekit

Lightweight NTP client for Swift. Get accurate server time on Apple platforms, immune to manual clock changes.

Features

  • Zero dependencies — only Foundation + Network.framework
  • async/await as the primary API, with completion handler wrappers
  • Thread-safe — fully Sendable, safe to use from any thread or actor
  • Monotonic clock — cached offset is immune to manual clock changes
  • Multi-sample sync — collects multiple NTP samples, picks the most accurate one
  • NTP v4 — implements RFC 5905 client mode
  • Swift 5 & 6 — strict concurrency safe

Requirements

  • iOS 16.0+
  • macOS 13.0+
  • tvOS 16.0+
  • watchOS 9.0+

Installation

Swift Package Manager

Add GlobalTimeKit to your Package.swift:

dependencies: [
    .package(url: "https://github.com/dimayurkovski/GlobalTimeKit.git", from: "1.0.0")
]

Or in Xcode: File → Add Package Dependencies and paste the repository URL.

CocoaPods

Add GlobalTimeKit to your Podfile:

pod 'GlobalTimeKit'

Usage

Quick Start

import GlobalTimeKit

let client = GlobalTimeClient()

// Sync with NTP server (collects 4 samples by default)
try await client.sync()

// Get corrected time instantly — no await, no network
let now = client.now

One-Shot Query

If you just need the server time once without caching:

let serverTime = try await GlobalTimeClient().fetchTime()

Custom Server

let config = GlobalTimeConfig(
    server: "time.google.com",
    timeout: .seconds(10),
    samples: 6
)
let client = GlobalTimeClient(config: config)
try await client.sync()

Check Sync Status

if client.isSynced {
    print("Offset: \(client.offset) seconds")
    print("Last sync: \(client.lastSyncDate!)")
    print("Server time: \(client.now)")
} else {
    print("Not synced, using system time")
}

Auto-Syncing Client

GlobalTimeAutoClient wraps GlobalTimeClient and automatically re-syncs when the network is restored or the app returns to the foreground:

let client = GlobalTimeAutoClient()

// First sync is explicit, as usual
try await client.sync()

// From now on, re-syncs happen automatically:
// — when network connectivity is restored
// — when the app becomes active (foreground)
// — with exponential backoff retry on failure
let now = client.now

Custom configuration:

let client = GlobalTimeAutoClient(config: GlobalTimeAutoConfig(
    server: "time.google.com",
    maxRetries: 5,
    retryBaseDelay: .seconds(2),
    alwaysReactToEvents: true
))

By default, auto re-sync only triggers after the first successful sync. Set alwaysReactToEvents: true to react to network and foreground events even before the first sync.

Both GlobalTimeClient and GlobalTimeAutoClient conform to GlobalTimeClientProtocol, so they can be used interchangeably:

let client: any GlobalTimeClientProtocol = GlobalTimeAutoClient()

Completion Handler API

For projects that don't use async/await:

client.sync { result in
    switch result {
    case .success:
        print("Synced! Offset: \(client.offset)")
    case .failure(let error):
        print("Error: \(error)")
    }
}

client.fetchTime { result in
    switch result {
    case .success(let date):
        print("Server time: \(date)")
    case .failure(let error):
        print("Error: \(error)")
    }
}

Error Handling

do {
    try await client.sync()
} catch let error as GlobalTimeError {
    switch error {
    case .timeout:
        print("Request timed out")
    case .invalidResponse:
        print("Invalid NTP response")
    case .dnsResolutionFailed:
        print("Could not resolve server hostname")
    case .networkUnavailable:
        print("No network connection")
    case .serverUnreachable:
        print("NTP server is unreachable")
    }
}

GMT Formatting

Get corrected time in GMT timezone, perfect for API requests and JWT tokens:

// Unix timestamp (seconds since 1970-01-01 UTC)
let timestamp = client.unixTimestamp
// Example: 1710598800.0

// ISO 8601 format
let iso = client.iso8601GMT
// Example: "2026-03-16T14:30:00Z"

// Custom format
let custom = client.formattedGMT("dd/MM/yyyy HH:mm")
// Example: "16/03/2026 14:30"

All formatting methods use en_US_POSIX locale and Gregorian calendar for consistent, locale-independent results.

How It Works

GlobalTimeKit sends a UDP packet to an NTP server and calculates the clock offset using the standard NTP formula:

offset = ((T2 - T1) + (T3 - T4)) / 2

Where T1–T4 are the four timestamps from the NTP exchange. The offset is cached locally, and client.now returns Date() + offset — no network call needed after sync.

Multiple samples are collected and the one with the lowest round-trip delay is selected for maximum accuracy.

API Reference

GlobalTimeClientProtocol

Both GlobalTimeClient and GlobalTimeAutoClient conform to this protocol.

| Property / Method | Description | | ------------------ | ----------------------------------------------------------- | | sync() | Sync with NTP server, cache the offset | | fetchTime() | One-shot query, returns server time without caching | | now | Corrected time using cached offset (falls back to Date()) | | unixTimestamp | Unix timestamp in GMT (seconds since 1970-01-01 UTC) | | iso8601GMT | ISO 8601 formatted time in GMT timezone | | formattedGMT(_:) | Custom formatted time in GMT timezone | | isSynced | Whether the client has synced at least once | | offset | Cached NTP offset in seconds | | lastSyncDate | Date of last successful sync |

GlobalTimeAutoClient

Takes a single GlobalTimeAutoConfig parameter (defaults to .init()).

GlobalTimeAutoConfig

| Parameter | Default | Description | | --------------------- | -------------------- | -------------------------------------------------------- | | server | "time.apple.com" | NTP server hostname | | timeout | .seconds(5) | Timeout for a single NTP request | | samples | 4 | Number of NTP samples to collect | | logLevel | .info | Diagnostic log verbosity | | maxRetries | 3 | Max retry attempts after a failed auto re-sync | | retryBaseDelay | .seconds(2) | Initial retry delay; doubles with each attempt | | alwaysReactToEvents | false | Re-sync on events even before the first successful sync |

GlobalTimeClient

| Property / Method | Description | | ------------------ | ----------------------------------------------------------- | | init(config:) | Create a client with optional configuration | | sync() | Sync with NTP server, cache the offset | | fetchTime() | One-shot query, returns server time without caching | | now | Corrected time using cached offset (falls back to Date()) | | unixTimestamp | Unix timestamp in GMT (seconds since 1970-01-01 UTC) | | iso8601GMT | ISO 8601 formatted time in GMT timezone | | formattedGMT(_:) | Custom formatted time in GMT timezone | | isSynced | Whether the client has synced at least once | | offset | Cached NTP offset in seconds | | lastSyncDate | Date of last successful sync |

GlobalTimeConfig

| Parameter | Default | Description | | ---------- | ------------------ | -------------------------------- | | server | "time.apple.com" | NTP server hostname | | timeout | .seconds(5) | Timeout for a single NTP request | | samples | 4 | Number of NTP samples to collect | | logLevel | .info | Diagnostic log verbosity |

GlobalTimeError

| Case | Description | | ---------------------- | ------------------------------------- | | .timeout | Request timed out | | .invalidResponse | Server returned an invalid NTP packet | | .dnsResolutionFailed | Could not resolve server hostname | | .networkUnavailable | No network connection | | .serverUnreachable | NTP server is unreachable |

License

GlobalTimeKit is available under the MIT license. See the LICENSE file for more information.

Package Metadata

Repository: dimayurkovski/globaltimekit

Default branch: main

README: README.md