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 / yy | Calendar year, 4 / 2 digits | 2009 / 09 |
| Y | Week-based year — differs from y near New Year | 2009 |
| MMMM | Month, full name | June |
| MMM | Month, abbreviated name | Jun |
| MM | Month number, 2 digits — month, not minute | 06 |
| M | Month number, no leading zero | 6 |
| dd / d | Day of month, 2 digits / no leading zero | 15 / 15 |
| D | Day of year — not day of month | 166 |
| EEEE | Day of week, full name | Monday |
| EEE | Day of week, abbreviated | Mon |
| HH / H | Hour, 24-hour clock, 2 digits / no zero | 13 / 13 |
| hh / h | Hour, 12-hour clock, 2 digits / no zero | 01 / 1 |
| mm / m | Minutes, 2 digits / no zero — minute, not month | 45 / 45 |
| ss / s | Seconds, 2 digits / no zero | 30 / 30 |
| SSS | Fraction of second, 3 digits (milliseconds) | 617 |
| a | AM/PM of day | PM |
| VV | Time-zone ID (IANA) | America/Los_Angeles |
| zzz | Time-zone name, short | PDT |
| zzzz | Time-zone name, full | Pacific Daylight Time |
| XXX | Offset, ISO 8601 — Z when zero | -07:00 |
| xxx | Offset — +00:00 when zero (never Z) | -07:00 |
| O | Localized zone-offset | GMT-7 |
| G | Era designator | AD |
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-digit | yyyy | .year() |
| Month number, 2-digit | MM | .month(.twoDigits) |
| Month, full name | MMMM | .month(.wide) |
| Day of month, 2-digit | dd | .day(.twoDigits) |
| Day of week, full | EEEE | .weekday(.wide) |
| Hour (12/24 per locale) | HH / hh | .hour() |
| Minute, 2-digit | mm | .minute() |
| Second, 2-digit | ss | .second() |
| ISO 8601 (whole value) | (the whole string) | .iso8601 |
Nothing matches that search. .
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
.deferredToDatewrites a 2001-relativeDouble(266791530.617) — almost never what an API wants. - Set
.iso8601on both the encoder and decoder — a mismatch throws on decode. - Built-in
.iso8601drops fractional seconds; use.millisecondsSince1970/.secondsSince1970for numeric-epoch APIs (mind the unit). PropertyListEncoderuses a native date type, so its default is fine — the trap isJSONEncoderspecifically.
Reference:
JSONEncoder.DateEncodingStrategy
and Date.ISO8601FormatStyle.
Showing 0 of 0 answers
No answers match that topic. .
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.
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-based — TimeZone(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 types — Sendable-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
Dateis a 2001-relativeDouble, not Unix 1970 —timeIntervalSince1970is the interop reading.JSONEncoder’s default encodes aDateas that 2001Double— set.iso8601on encoder and decoder.- A fixed-format
DateFormatterneedsLocale(identifier: "en_US_POSIX")or it breaks on a user’s 12/24-hour or non-Gregorian setting. addingTimeInterval(86400)is raw seconds; useCalendar.date(byAdding:)for a DST-correct “one day.”- You can’t read year/month/day off a
Datewithout aCalendar. DateFormatteris expensive to construct — cache and reuse a configured instance.- The Unicode letter traps:
MMmonth vsmmminute;HH24-hour vshh12-hour;y/Yandd/Dare different fields. - Never measure elapsed time with
Date()differences — useContinuousClock(or uptime pre-5.7); the wall clock jumps. FormatStyle/Date.nowneed iOS 15;Duration/Clockneed iOS 16 / Swift 5.7 — gate with#available.- Represent “no date” with a nullable
Date?, not a sentinel likedistantPast.
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