Swift reference · Dates & times

Swift Date & Time Handling

The Swift answer to “how do I do dates?” in one place — the Date + Calendar + TimeZone + DateComponents model, FormatStyle vs DateFormatter formatting, parsing, time zones, UTC, arithmetic, the 2001-vs-Unix epoch, Codable, and Apple-OS availability.

Which do I reach for?

Foundation keeps one instant type and routes all calendar work through Calendar. A Date alone can’t tell you the year — the mental model below is the whole game.

Date

An absolute point in time — a Double of seconds, and famously from a 2001 reference date, not Unix 1970. Always the type you store and pass around.

Calendar + DateComponents

Anything calendar-aware — extracting the year/month/day, adding “one month,” start of day. Calendar carries a TimeZone and Locale. There is no native LocalDate; wall values are DateComponents.

FormatStyle / DateFormatter

Strings. Reach for the modern, type-safe FormatStyle (date.formatted(…), iOS 15+); drop to DateFormatter for older targets or fixed machine formats — and then set en_US_POSIX.

Duration + ContinuousClock

Durations and elapsed timing (Swift 5.7+). Use ContinuousClock().measure { } for “how long did that take,” never a difference of two Date() readings (wall clock, can jump).

Formatting reference

The most-searched date answer: “how do I format the date?” The legacy answer is DateFormatter’s Unicode pattern-letter mini-language — set formatter.dateFormat = "yyyy-MM-dd HH:mm". The modern, recommended path is the type-safe FormatStyle (iOS 15+); the second table maps the same fields to it. New code should prefer FormatStyle — the pattern table is what people search for, but it’s the API to steer away from.

0 of 0 rows

Example output is the canonical instant — Monday, June 15, 2009 1:45:30.617 PM at offset -07:00 (i.e. 2009-06-15T20:45:30.617Z) — a Date rendered in America/Los_Angeles with Locale(identifier: "en_US"), so the wall clock reads 1:45:30 PM. Month/weekday names are locale-sensitive.

Legacy — DateFormatter.dateFormat Unicode pattern letters

Pattern Meaning Example
yyyy / yyCalendar year, 4 / 2 digits2009 / 09
YWeek-based year — differs from y near New Year2009
MMMMMonth, full nameJune
MMMMonth, abbreviated nameJun
MMMonth number, 2 digits — month, not minute06
MMonth number, no leading zero6
dd / dDay of month, 2 digits / no leading zero15 / 15
DDay of yearnot day of month166
EEEEDay of week, full nameMonday
EEEDay of week, abbreviatedMon
HH / HHour, 24-hour clock, 2 digits / no zero13 / 13
hh / hHour, 12-hour clock, 2 digits / no zero01 / 1
mm / mMinutes, 2 digits / no zero — minute, not month45 / 45
ss / sSeconds, 2 digits / no zero30 / 30
SSSFraction of second, 3 digits (milliseconds)617
aAM/PM of dayPM
VVTime-zone ID (IANA)America/Los_Angeles
zzzTime-zone name, shortPDT
zzzzTime-zone name, fullPacific Daylight Time
XXXOffset, ISO 8601 — Z when zero-07:00
xxxOffset — +00:00 when zero (never Z)-07:00
OLocalized zone-offsetGMT-7
GEra designatorAD

The en_US_POSIX rule. A DateFormatter used for a fixed machine format (an ISO string, a filename stamp) must set formatter.locale = Locale(identifier: "en_US_POSIX"). Without it the formatter honors the user’s settings — a 12/24-hour preference or a non-Gregorian calendar can silently rewrite "yyyy-MM-dd" into something you never intended. This is the single most-copied wrong snippet on the open web. Full grammar: Unicode TR35 date field symbols · Apple QA1480 (en_US_POSIX).

Modern — the FormatStyle field builder iOS 15+

Field DateFormatter pattern FormatStyle call
Year, 4-digityyyy.year()
Month number, 2-digitMM.month(.twoDigits)
Month, full nameMMMM.month(.wide)
Day of month, 2-digitdd.day(.twoDigits)
Day of week, fullEEEE.weekday(.wide)
Hour (12/24 per locale)HH / hh.hour()
Minute, 2-digitmm.minute()
Second, 2-digitss.second()
ISO 8601 (whole value)(the whole string).iso8601

So 2009-06-15 13:45 is formatter.dateFormat = "yyyy-MM-dd HH:mm" the legacy way, and the modern way is date.formatted(.dateTime.year().month().day().hour().minute()) — which composes fields with locale-appropriate separators, so you don’t hand-write them. The presets date.formatted(date: .abbreviated, time: .shortened) and date.formatted(.iso8601) cover the common shapes, and FormatStyle also parses. Both models share the Unicode case traps: MM is month but mm is minute; HH is 24-hour but hh is 12-hour; and y (calendar year) / Y (week-year) and d (day of month) / D (day of year) are four different fields. Reference: Date.FormatStyle.

JSON & API timestamps

That 2009-06-15T20:45:30Z shape in JSON payloads is ISO 8601 in UTC (the trailing Z, “Zulu,” is the +00:00 offset). Here is the Swift foot-gun that dominates real bugs: JSONEncoder’s default silently encodes a Date as a number, not that string. Set the strategy on both ends.

Write — set .iso8601

let enc = JSONEncoder()
// DEFAULT (.deferredToDate) encodes a Double:
// {"at":266791530.617}  ← 2001-relative, NOT a string

enc.dateEncodingStrategy = .iso8601
try enc.encode(event)
// {"at":"2009-06-15T20:45:30Z"}

The built-in .iso8601 omits fractional seconds — use .custom to keep the .617.

Read — match the decoder

let dec = JSONDecoder()
dec.dateDecodingStrategy = .iso8601
let event = try dec.decode(Event.self, from: data)

// Or a raw ISO round-trip, no Codable:
let at = try Date(
    "2009-06-15T20:45:30Z", strategy: .iso8601)

Encoder and decoder strategies must agree, or the round-trip fails.

  • The default .deferredToDate writes a 2001-relative Double (266791530.617) — almost never what an API wants.
  • Set .iso8601 on both the encoder and decoder — a mismatch throws on decode.
  • Built-in .iso8601 drops fractional seconds; use .millisecondsSince1970 / .secondsSince1970 for numeric-epoch APIs (mind the unit).
  • PropertyListEncoder uses a native date type, so its default is fine — the trap is JSONEncoder specifically.

Reference: JSONEncoder.DateEncodingStrategy and Date.ISO8601FormatStyle.

Showing 0 of 0 answers

1 · The types & the model

Why can’t I get the year out of a Date?

Because a Date holds only an absolute point in time — a TimeInterval (a Double of seconds), and relative to a 2001 reference date, not the Unix 1970 epoch. It has no notion of a calendar, a zone, or a year. To read a year, month, or day you go through a Calendar (which carries a TimeZone and Locale) and get DateComponents back. There is no native LocalDate / LocalTime type: a wall value is a DateComponents.

let now = Date()                       // an instant, nothing else
let comps = Calendar.current.dateComponents(
    [.year, .month, .day], from: now)
comps.year                            // 2009  (needs a Calendar)

// The two formatting worlds on top of the model:
now.formatted(.dateTime.year().month().day())   // FormatStyle
// vs a DateFormatter with a dateFormat pattern (legacy)

So the model is four pieces: Date (the instant), Calendar (+ its TimeZone and Locale), TimeZone, and DateComponents. Everything calendar-aware routes through them.

Source: Date / Calendar.

2 · Getting “now”

How do I get the current date/time?

Date() is the current instant on every OS version; Date.now iOS 15+ reads the same thing more legibly. For testable code, don’t call Date() inline — inject a date provider (a closure or a Clock) so a test can hand in a fixed time.

Date()                         // now — every OS
Date.now                        // same, iOS 15+

// Testable: take a provider, don't call Date() inside
struct Service {
    var now: () -> Date = { .now }
    func stamp() -> Date { now() }
}

Source: Date.now.

3 · Formatting

How do I format a date to a string?

Two models, per the reference above. The modern, recommended path is FormatStyle iOS 15+date.formatted(…) with field builders or the date:/time: presets. For older targets or a fixed machine format, use DateFormatter with a dateFormat pattern — and set en_US_POSIX. DateFormatter is expensive to construct, so build it once and reuse it.

// Modern — FormatStyle
date.formatted(date: .abbreviated, time: .shortened)
date.formatted(.dateTime.year().month().day())

// Legacy — fixed format needs en_US_POSIX; cache the formatter
let f = DateFormatter()
f.locale = Locale(identifier: "en_US_POSIX")
f.timeZone = TimeZone(identifier: "America/Los_Angeles")
f.dateFormat = "yyyy-MM-dd HH:mm"
f.string(from: date)          // "2009-06-15 13:45"

Source: Date.FormatStyle / DateFormatter.

4 · Parsing

How do I parse a date string?

ISO 8601 is the portable interchange format. The modern path is Date(_:strategy:) / ParseStrategy iOS 15+ — the same FormatStyle that formats also parses. The legacy path is DateFormatter.date(from:) or ISO8601DateFormatter. For any fixed input, set en_US_POSIX or the parse breaks on a user’s locale.

// Modern
try Date("2009-06-15T20:45:30Z", strategy: .iso8601)
try Date("06/15/2009", strategy: Date.FormatStyle()
        .month().day().year().parseStrategy)

// Legacy — fixed input, en_US_POSIX
let f = DateFormatter()
f.locale = Locale(identifier: "en_US_POSIX")
f.dateFormat = "yyyy-MM-dd"
f.date(from: "2009-06-15")      // Date?

Source: Date.ISO8601FormatStyle / ISO8601DateFormatter.

5 · Time zones

How do I work with time zones?

Foundation zones are IANA-basedTimeZone(identifier: "America/Los_Angeles") — so there is no Windows-vs-IANA zone-id tripwire. Read the system zone with TimeZone.current, and UTC with TimeZone.gmt (or TimeZone(secondsFromGMT: 0) on older targets). The instant itself is zone-agnostic; you attach a zone via the Calendar or the formatter when you want a wall value. Around DST a local time can be ambiguous (fall-back) or skipped (spring-forward); Calendar resolves these deterministically.

var cal = Calendar(identifier: .gregorian)
cal.timeZone = TimeZone(identifier: "America/Los_Angeles")!
cal.dateComponents([.hour], from: date).hour   // 13 (wall clock)

TimeZone.current               // the device zone
TimeZone(abbreviation: "PDT")  // also available

Source: TimeZone.

6 · Converting to/from UTC

How do I convert to and from UTC?

There is nothing to convert on the Date itself — it is already an absolute instant, and UTC is just the zone you read it in. To read the UTC wall clock, point a Calendar (or a formatter) at TimeZone.gmt and pull the components. To build a Date from UTC components, do the same in reverse.

var cal = Calendar(identifier: .gregorian)
cal.timeZone = .gmt
cal.dateComponents([.hour, .minute], from: date)  // 20:45 (UTC)

// Build a Date from UTC wall components:
var c = DateComponents()
c.year = 2009; c.month = 6; c.day = 15
c.hour = 20; c.minute = 45; c.timeZone = .gmt
cal.date(from: c)                                 // the instant

Source: Calendar.dateComponents(_:from:).

7 · Arithmetic

How do I add or subtract time?

Two paths, and the difference is load-bearing. Calendar.date(byAdding:value:to:) is calendar- and DST-aware — “one day later” lands on the same wall time even across a DST change. date.addingTimeInterval(_:) adds raw seconds, so +86400 is exactly 24 hours — which is not “one day” on a DST boundary (it’s 23 or 25 wall hours). Use a calendar unit for “same time tomorrow,” the interval for “exactly 24 hours later.”

let cal = Calendar.current
cal.date(byAdding: .day, value: 1, to: date)   // calendar/DST-aware
cal.date(byAdding: DateComponents(month: 1, day: 3), to: date)

date.addingTimeInterval(86400)             // raw +24h — the trap

Source: Calendar.date(byAdding:value:to:).

8 · Differences between two dates

How do I get the difference between two dates?

For calendar units (whole days, months, years), ask a Calendar with dateComponents([.day], from:to:). For a raw elapsed span in seconds, use timeIntervalSince(_:) — a Double. The distinction matters for the same reason arithmetic does: “how many calendar days” is not always “seconds ÷ 86400.”

let days = Calendar.current
    .dateComponents([.day], from: a, to: b).day   // 30

let secs = b.timeIntervalSince(a)                 // Double seconds

Source: Calendar.dateComponents(_:from:to:) / timeIntervalSince(_:).

9 · Comparison & equality

How do I compare two dates — does == work?

Yes — cleanly. Date is a Comparable and Equatable value type, so <, >, ==, .compare(_:), and min/max all work on the underlying instant, no reference-identity surprise (the opposite of JavaScript’s === trap). Two Dates built from the same moment are equal.

let a = Date(timeIntervalSince1970: 1245098730.617)
let b = Date(timeIntervalSince1970: 1245098730.617)
a == b            // true — value equality
a < b             // false
min(a, b)         // earliest

One caveat: two Dates parsed from strings can differ in the last bits of the Double; if you’re comparing to the second, compare timeIntervalSince within a tolerance.

Source: Date.compare(_:).

10 · Min / max & “no value”

What are the bounds, and how do I represent “no date”?

The far-bound sentinels are Date.distantPast and Date.distantFuture — use them as “effectively unbounded” edges, not as an absence marker. For “no value,” the idiomatic Swift answer is an optional Date? and nil — the type system encodes absence, so there is no magic sentinel like C#’s MinValue or JavaScript’s Invalid Date.

var shippedOn: Date? = nil          // no date yet — no sentinel
if let d = shippedOn { render(d) }     // only if present
let due = shippedOn ?? .distantFuture // explicit fallback

Source: Date.distantPast / Swift Optional.

11 · Unix / epoch time

How do I convert to/from a Unix timestamp? What’s the 2001 thing?

The headline Swift trap: Date has two epoch readings. timeIntervalSince1970 is the Unix epoch (what every API and other language means by “timestamp”). timeIntervalSinceReferenceDate is Foundation’s own 2001-01-01 epoch — the number Date stores internally, and the one the default JSONEncoder leaks. For interop, always use the 1970 reading. Seconds are a Double, so sub-second precision is built in, and there is no year-2038 problem (it’s a 64-bit float, not a 32-bit int).

Date(timeIntervalSince1970: 1245098730.617)
// → 2009-06-15T20:45:30.617Z
date.timeIntervalSince1970          // 1245098730.617  (Unix)
date.timeIntervalSinceReferenceDate // 266791530.617   (2001!)

// A millisecond API? multiply/divide by 1000:
Date(timeIntervalSince1970: ms / 1000)

Source: timeIntervalSince1970 / timeIntervalSinceReferenceDate.

12 · Precision & measuring elapsed time

How do I time an operation? (Why not two Date() reads?)

Use ContinuousClock().measure { } Swift 5.7+, which returns a Duration from a monotonic clock. Never subtract two Date() readings for elapsed time: the wall clock can jump backward on an NTP sync or a manual change, giving a negative or wrong duration. SuspendingClock is the variant that pauses while the device sleeps. On pre-Clock targets, reach for DispatchTime, ProcessInfo.processInfo.systemUptime, or mach_absolute_time.

let elapsed = ContinuousClock().measure {
    doWork()
}                                 // Duration — monotonic

// Pre-5.7: uptime is monotonic too
let t0 = ProcessInfo.processInfo.systemUptime
doWork()
let secs = ProcessInfo.processInfo.systemUptime - t0
// Never: Date() differences for elapsed — wall clock jumps

Source: ContinuousClock / Duration.

13 · Immutability & thread-safety

Are the types safe to share across threads?

Date, Calendar, TimeZone, and DateComponents are immutable value typesSendable-friendly and Codable — so passing them around is free of shared-state hazards. The one thing to watch is DateFormatter: it is safe to read concurrently once fully configured, but it is expensive to construct, so cache and reuse a configured instance rather than making one per call. (This is the Foundation analog of Java’s thread-unsafe SimpleDateFormat lesson — here the concern is cost, not corruption.)

// Build once, reuse — construction is the expensive part
enum Fmt {
    static let iso: ISO8601DateFormatter = {
        let f = ISO8601DateFormatter()
        return f
    }()
}

Source: DateFormatter (thread-safety notes).

14 · Serialization (Codable)

How do dates serialize with Codable?

Date is Codable, but — per the JSON block above — the JSONEncoder default (.deferredToDate) writes a 2001-relative Double, not an ISO string. Set dateEncodingStrategy = .iso8601 (and the matching dateDecodingStrategy), or .millisecondsSince1970 / .secondsSince1970 / .formatted(_:) / .custom as the API requires. PropertyListEncoder is different — it has a native date type, so its default is already correct.

struct Event: Codable { let at: Date }

let enc = JSONEncoder()
enc.dateEncodingStrategy = .iso8601        // not the default!
try enc.encode(Event(at: date))
// {"at":"2009-06-15T20:45:30Z"}

Source: JSONEncoder.DateEncodingStrategy.

15 · Common recipes

Start of day, end of month, age, is-weekend, next Monday?

Nearly every everyday recipe is a Calendar method — another reason the Calendar is the workhorse of the model.

let cal = Calendar.current

cal.startOfDay(for: date)                       // midnight, local

// Age from a birthdate:
cal.dateComponents([.year], from: birth, to: .now).year

cal.isDateInWeekend(date)                        // Bool

// Next Monday at/after date:
cal.nextDate(after: date,
    matching: DateComponents(weekday: 2),
    matchingPolicy: .nextTime)

// Range of days in the month (→ end of month):
cal.range(of: .day, in: .month, for: date)       // 1..<31

Source: Calendar.nextDate(after:matching:…) / isDateInWeekend(_:).

16 · Availability & cross-platform

Which modern APIs are OS-gated — and what about Swift off Apple platforms?

Swift’s signature date dimension. The modern APIs carry deployment-target floors: FormatStyle and Date.now need iOS 15 / macOS 12 (2021); Duration and the Clock protocol need Swift 5.7 / iOS 16 / macOS 13 (2022). Below those floors, fall back to DateFormatter and uptime-based timing. Gate a call with if #available or an @available annotation.

if #available(iOS 15, macOS 12, *) {
    date.formatted(.dateTime.year().month().day())
} else {
    // DateFormatter fallback for older targets
}

On non-Apple platforms — server-side Swift (Vapor), Linux, Windows — date behavior used to diverge because Foundation was Objective-C-bridged. That’s largely over: the open-source swift-foundation rewrite provides Swift-native Calendar, TimeZone, Locale, JSONEncoder, and the formatting stack (in the FoundationEssentials / FoundationInternationalization modules), shared across every platform — so date behavior is now far more consistent than in the bridged era.

Source: Date.FormatStyle availability / swift-foundation. Related: iOS versions (the availability floor) and Swift versions (where Duration / Clock arrived in 5.7).

The foot-guns, in one place

  • Date is a 2001-relative Double, not Unix 1970 — timeIntervalSince1970 is the interop reading.
  • JSONEncoder’s default encodes a Date as that 2001 Doubleset .iso8601 on encoder and decoder.
  • A fixed-format DateFormatter needs Locale(identifier: "en_US_POSIX") or it breaks on a user’s 12/24-hour or non-Gregorian setting.
  • addingTimeInterval(86400) is raw seconds; use Calendar.date(byAdding:) for a DST-correct “one day.”
  • You can’t read year/month/day off a Date without a Calendar.
  • DateFormatter is expensive to construct — cache and reuse a configured instance.
  • The Unicode letter traps: MM month vs mm minute; HH 24-hour vs hh 12-hour; y/Y and d/D are different fields.
  • Never measure elapsed time with Date() differences — use ContinuousClock (or uptime pre-5.7); the wall clock jumps.
  • FormatStyle/Date.now need iOS 15; Duration/Clock need iOS 16 / Swift 5.7 — gate with #available.
  • Represent “no date” with a nullable Date?, not a sentinel like distantPast.

Every answer links its primary source inline — the Foundation date documentation for Date / Calendar / TimeZone / DateComponents / DateFormatter / FormatStyle, the Swift standard library for Duration / Clock, the Unicode TR35 date field symbols for the pattern table, and swift-foundation for cross-platform behavior. The Duration / Clock timeline cross-links the Swift version reference. As of July 2026: FormatStyle and Date.now require iOS 15 / macOS 12 (2021); Duration and the Clock protocol require Swift 5.7 / iOS 16 / macOS 13 (2022); and swift-foundation — the Swift-native Foundation rewrite — ships in the toolchain across Apple, Linux, and Windows, so Calendar, Locale, JSONEncoder, and formatting are now Swift implementations.

Mungomash LLC · More on Swift