DateFormatter is a Foundation class designed for bidirectional conversion between Date objects and their string representations. It takes into account the user's locale, time zone and calendar, ensuring correct date display in any region of the world. According to Apple Developer Documentation (2025), DateFormatter supports four preset date and time styles as well as fully custom formats through a template string. Without DateFormatter it is impossible to correctly display a date to the user in an internationalized application.
Key Takeaways
DateFormatter is a class from the Foundation framework that implements bidirectional conversion between Date and string. It first appeared in OpenStep as NSDateFormatter and has remained the primary tool for date formatting across all Apple platforms. The class inherits from Formatter and provides a convenient API for localized date display.
DateFormatter works based on Unicode LDML patterns — the same ones used in ICU (International Components for Unicode). The pattern is set via the dateFormat property, where symbols y, M, d, H, m, s correspond to year, month, day, hours, minutes, seconds. Repeating a symbol determines the format: "y" — two-digit year, "yyyy" — four-digit year.
Creating a DateFormatter is an expensive operation because locale and calendar data are loaded during initialization. Apple recommends creating a formatter once for each formatting type and reusing it. In SwiftUI and UIKit, formatters are often cached in static properties or created lazily on first access.
DateFormatter is used in many system iOS components. UIDatePicker uses DateFormatter internally to display dates in countDownTimer mode. A TextField with a formatter can automatically validate user-entered dates. Core Data supports Date type attributes, but their string representation is always handled through DateFormatter.
Thread Safety — DateFormatter is not thread-safe. Modifying formatter properties from different threads leads to undefined behavior. For multithreaded use, create separate formatter instances for each thread or use synchronization via NSLock or a serial queue.
dateStyle and timeStyle are the simplest ways to configure date display. Each style has four variants: .short, .medium, .long, .full. The combination of dateStyle and timeStyle allows independent configuration of date and time format, and the .none property disables the corresponding part.
For the US locale, .short formats the date as "7/21/26", and for the Russian locale as "21.07.2026". The .long style for the Russian locale outputs "July 21, 2026", and .full outputs "Tuesday, July 21, 2026" with the day of the week. All four styles automatically adapt to regional standards, including component order and separators.
SFDateFormatter in iOS 15+ provides an alternative approach via RelativeDateFormatter and DateIntervalFormatter. RelativeDateFormatter outputs "today", "yesterday", "in 3 days" for contextual display. DateIntervalFormatter displays date ranges: "July 21–25, 2026" — for bookings and planning.
| Style | Example (ru_RU) | Example (en_US) |
|---|---|---|
| .short | 21.07.2026 | 7/21/26 |
| .medium | Jul 21, 2026 | Jul 21, 2026 |
| .long | July 21, 2026 | July 21, 2026 |
| .full | Tuesday, July 21, 2026 | Tuesday, July 21, 2026 |
When combining styles, DateFormatter automatically selects the separator: for .short.date + .short.time the result might be "7/21/26, 2:30 PM". For .full.date + .full.time — "Tuesday, July 21, 2026, 2:30:00 PM GMT+3". The separator is managed by the locale, not the developer — this ensures compliance with regional user expectations.
dateFormat allows setting an arbitrary formatting pattern using Unicode LDML specification symbols. This gives full control over display: you can show only the year and month, or the day of the week without the date, or time without seconds. Custom format is indispensable for specific design requirements.
Main symbols — yyyy (year: 2026), MM (month: 07), dd (day: 21), HH (hours: 14), mm (minutes: 30), ss (seconds: 00). For the full month name use MMMM (July), for abbreviated — MMM (Jul). Day of the week — EEEE (Tuesday), abbreviated — E (Tue).
When using dateFormat it is important to set the formatter's locale. If locale is not set, the formatter uses the system locale, which may be undesirable for a fixed format in an API. Apple recommends setting locale = Locale(identifier: "en_US_POSIX") for a fixed cross-regional format, especially when parsing dates from server responses.
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "ru_RU")
formatter.dateFormat = "d MMMM yyyy"
let customString = formatter.string(from: Date())
// "21 July 2026"
// Parsing a custom string
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
let date = formatter.date(from: "2026-07-21 14:30:00")!
A mistake in dateFormat is one of the common causes of app crashes. If the format does not match the string, the date(from:) method returns nil. Use guard let or ?? for safe optional unwrapping. To validate the format, test it on all supported languages — some LDML symbols work differently in different locales.
Locale determines how month names, day names and separators are displayed. DateFormatter uses Locale.current by default, but in some scenarios a specific locale needs to be specified: for a fixed format in logs use en_US_POSIX, for server dates — the locale matching the server.
The TimeZone property determines the time zone for display. By default the system time zone is used, but for applications with an international audience dates often need to be displayed in the user's time zone or in UTC. Changing the timeZone only affects display — the Date value remains unchanged.
An important feature: if DateFormatter is used for parsing a string and the string contains a time zone indication (for example, "2026-07-21T14:30:00Z" with Z for UTC), the timeZone property is ignored — the formatter uses the time zone from the string. If the time zone is absent from the string, the formatter's timeZone is applied.
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "ru_RU")
formatter.timeZone = TimeZone(identifier: "Europe/Moscow")
formatter.dateStyle = .long
formatter.timeStyle = .short
let moscowTime = formatter.string(from: Date())
// "21 July 2026, 14:30"
// Parsing without timezone in string
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd HH:mm"
let utcDate = formatter.date(from: "2026-07-21 10:30")!
AutoupdatingCurrentLocale — a special locale type that automatically updates when the user's system settings change. DateFormatter supports it by default. If the app is running in the background and the user changes the system language, a formatter created before the change will continue using the old locale — a new instance needs to be created to update.
ISO8601DateFormatter is a specialized formatter for working with dates in ISO 8601 format. This format is the de facto standard for REST APIs, JSON and data exchange. ISO8601DateFormatter works significantly faster than DateFormatter because it does not depend on locale and uses a fixed parsing grammar.
Main formatter options — .withInternetDateTime (2026-07-21T14:30:00Z), .withFractionalSeconds (adds milliseconds), .withTimeZone (includes time zone offset). By combining options you can get any ISO 8601 variant: with milliseconds, with time zone, with date only.
JSONEncoder.DateEncodingStrategy allows globally configuring date encoding for all Codable models. Options — .iso8601 (uses ISO8601DateFormatter), .formatted(DateFormatter), .millisecondsSince1970, .secondsSince1970. The strategy choice affects the entire serialization lifecycle and should be consistent across all API endpoints.
// ISO8601DateFormatter
let isoFormatter = ISO8601DateFormatter()
isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let isoString = isoFormatter.string(from: Date())
// "2026-07-21T14:30:00.000Z"
// JSONEncoder with ISO8601
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
// Alternative: JSONEncoder with custom formatter
let customEncoder = JSONEncoder()
customEncoder.dateEncodingStrategy = .formatted(myFormatter)
DateFormatter vs ISO8601DateFormatter — choose ISO8601DateFormatter for serializing and parsing dates in API, as it is 5-10 times faster than DateFormatter and is not prone to localization errors. Use DateFormatter for the user interface where localized display with month and day names in the user's native language is required.
Let's look at real-world scenarios for using DateFormatter in an iOS app: displaying dates in a news feed, entering a birth date, and exporting a report with dates in different time zones.
RelativeDateFormatter is optimal for news feeds. It displays "just now", "5 minutes ago", "yesterday" for fresh news and switches to the full date for older ones. The switch threshold is configured via calendar: for news use a 24-hour threshold, for messengers — a week.
func formatRelativeDate(_ date: Date) -> String {
let relative = RelativeDateFormatter()
relative.unitsStyle = .full
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
let daysDiff = Calendar.current.dateComponents(
[.day], from: date, to: Date()
).day ?? 0
return daysDiff < 1
? relative.localizedString(for: date, relativeTo: Date())
: formatter.string(from: date)
}
Entering a birth date — another common scenario. DateFormatter is configured with a specific dateFormat "dd.MM.yyyy" and locale "ru_RU". When parsing the entered string it is important to handle possible errors: the formatter returns nil for an invalid string. After successful parsing, the date is checked to be within an acceptable range — not earlier than 1900, not later than today.
Exporting a report with dates requires a fixed format independent of the user's locale. Use dateFormat "yyyy-MM-dd HH:mm:ss" with locale en_US_POSIX and time zone UTC. This approach guarantees the file will open correctly in any country regardless of regional system settings.
| Scenario | Formatter | Key Setting |
|---|---|---|
| News feed | RelativeDateFormatter | unitsStyle = .full |
| Date input | DateFormatter | dateFormat + fallback |
| API serialization | ISO8601DateFormatter | withInternetDateTime |
| Report export | DateFormatter | en_US_POSIX + UTC |
Frequently Asked Questions
The most common reason — a mismatch between dateFormat and the string format. For example, the format "dd.MM.yyyy" will not parse the string "2026-07-21". The second reason — locale mismatch: the string "July 21, 2026" will not parse with the ru_RU locale. The third — typos in LDML symbols: use yyyy, not YYYY (different meaning).
No. DateFormatter is a heavy object, its initialization includes loading locale data. Create one instance per formatting type and reuse it. In a multithreaded environment use Thread-local storage or a formatter pool with a serial queue for synchronization.
DateFormatter displays an absolute date (July 21, 2026), while RelativeDateFormatter displays a relative one (today, yesterday, in 3 days). RelativeDateFormatter was introduced in iOS 15+ and uses the same LDML template but automatically selects relative display.
Set the formatter's timeZone to UTC before parsing. If the server returns a date in local time without time zone indication, check the API specification — most likely UTC is implied. For ISO 8601 with Z at the end, timeZone is not needed — the formatter parses the offset from the string.
Do not use a single instance from different threads without synchronization. Create a new instance in each thread or use Thread.current.threadDictionary for storage. An alternative is NSLock with locking for the duration of string(from:) and date(from:).
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also