Swift

Foundation’s Date, Calendar/DateComponents, and the en_US_POSIX trap.

Swift’s standard library has no date and time types — they live in Foundation (import Foundation, now also available through the open-source swift-foundation). The cornerstone is Date: a single point in time stored as a TimeInterval (a Double of seconds) relative to a fixed reference instant, 2001-01-01T00:00:00 UTC. Crucially, a Date carries no time zone — it is an absolute instant, the equivalent of an “aware UTC” value, much like a Unix timestamp. Date() (or Date.now) gives you the current moment.

Because Date is purely an instant, the civil side — “what does a human read off the wall clock?” — is a separate concern handled by DateComponents (a bag of year/month/day/hour/… fields) together with a Calendar, which itself holds a TimeZone. You never convert between an instant and civil fields without naming a calendar and zone. See Instant vs civil time and Time zones vs offsets.

A Date names a point; a calendar day names a period. Foundation models the period explicitly with DateInterval (a start Date plus a duration), and calendar.dateInterval(of: .day, for: date) gives you the whole day a moment falls in. See Points vs periods.

The single biggest source of Swift date bugs is DateFormatter and the user’s locale. A formatter with a fixed format string ("yyyy-MM-dd") still uses the device’s locale and calendar by default — so for a user on a Buddhist or Japanese calendar, or a 12-hour clock, parsing and formatting silently produce wrong results. For machine-readable, fixed formats you must pin the locale to en_US_POSIX, or better, use ISO8601DateFormatter.

Date vs DateComponents

Since Date is an instant and DateComponents is the civil reading, the naive/aware confusion shows up at the conversion boundary rather than in arithmetic on a single value. Two hazards dominate: using Calendar.current / TimeZone.current when you meant a fixed business zone (results drift with device settings), and treating print(date) output as local — Date.description is always rendered in UTC, which routinely surprises people debugging in a non-UTC zone.

Pitfall: A DateFormatter with a fixed format string but no explicit locale uses the device locale and calendar. On a device set to a non-Gregorian calendar or 12-hour clock, parsing an ISO-style string fails or yields the wrong date. Set formatter.locale = Locale(identifier: "en_US_POSIX") for any fixed-format parsing, or use ISO8601DateFormatter. See parsing user input.

Pitfall: date.addingTimeInterval(86400) advances exact elapsed seconds, not civil time — across a spring-forward night it lands an hour off “same time tomorrow”. For DST-aware calendar arithmetic use calendar.date(byAdding: .day, value: 1, to: date) instead.

See also: Instant vs civil time and Parsing user input.

Go deeper: Calendar conversions, formatting, and DST-safe arithmetic

Instant ⇄ civil fields go through a Calendar + TimeZone:

import Foundation

var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(identifier: "Europe/Paris")!

let now = Date()                                            // an absolute instant

// instant -> civil reading
let parts = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: now)

// civil reading -> instant (a nonexistent DST-gap time is silently resolved
// forward, not rejected — see the caveat below)
var c = DateComponents()
c.year = 2026; c.month = 6; c.day = 5; c.hour = 14
let instant = calendar.date(from: c)

Parsing and formatting. Prefer ISO8601DateFormatter for interchange and the modern Date.FormatStyle API for display:

// Machine interchange — fixed, locale-independent
let iso = ISO8601DateFormatter()
let d = iso.date(from: "2026-06-05T14:00:00Z")

// Legacy fixed-format parsing — MUST pin the locale
let df = DateFormatter()
df.locale = Locale(identifier: "en_US_POSIX")
df.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
df.timeZone = TimeZone(identifier: "Europe/Paris")

// Display in the user's locale (iOS 15+ / recent Foundation)
let shown = Date().formatted(date: .long, time: .shortened)

Exact vs calendar arithmetic — the distinction behind pitfall #2:

let exact   = now.addingTimeInterval(24 * 60 * 60)              // exactly 24h later
let nextDay = calendar.date(byAdding: .day, value: 1, to: now)  // "same time tomorrow", DST-aware
// Across the spring-forward night these are different instants.

(One caveat shared by every language: “same wall-clock time tomorrow” can land in a DST gap or fold, where the civil reading doesn’t exist or is ambiguous. calendar.date(from:) does not fail for a nonexistent time — it silently shifts forward past the gap, e.g. 02:30 on a US spring-forward night resolves to 03:30. Observed on both Apple’s Foundation and the open-source swift-foundation.)

If you want the compiler to hold the instant/civil line for you, the third-party Time library (Dave DeLong) models calendar values as distinct generic types — conceptually the closest thing Swift has to Temporal or kotlinx-datetime. Foundation used correctly covers most applications; reach for Time when a codebase keeps mixing up instants and civil values.

Note that Swift’s standard library Duration type (Swift 5.7+) is for high-precision elapsed-time measurement, not calendar arithmetic — reach for Calendar and DateComponents for anything civil.

See also Time zones vs offsets and DST.


← Back to all topics