DateFormatter: Key Concepts, Date Formatting and Localization

Author: IT Sectr Published: 2026-07-12 Reading time: 7 min

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 — a class for converting Date to string and back with locale and time zone support.
  • dateStyle and timeStyle — preset styles (.short, .medium, .long, .full) for quick formatting.
  • dateFormat — a template string for custom format specified via Unicode LDML symbols.
  • Locale and TimeZone — formatter properties that determine regional display and time zone.
  • ISO8601DateFormatter — a faster alternative for ISO 8601 format when serializing in API.

What is DateFormatter?

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 in iOS SDK

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.

DateFormatter Formatting Styles

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.

StyleExample (ru_RU)Example (en_US)
.short21.07.20267/21/26
.mediumJul 21, 2026Jul 21, 2026
.longJuly 21, 2026July 21, 2026
.fullTuesday, July 21, 2026Tuesday, 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.

Custom Formats via dateFormat

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.

swift
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.

Localization and TimeZone

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.

swift
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 and Alternatives

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.

swift
// 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.

Date Formatting Examples

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.

Displaying news date in a list

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.

swift
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.

ScenarioFormatterKey Setting
News feedRelativeDateFormatterunitsStyle = .full
Date inputDateFormatterdateFormat + fallback
API serializationISO8601DateFormatterwithInternetDateTime
Report exportDateFormatteren_US_POSIX + UTC

Frequently Asked Questions

Why does DateFormatter return nil for a valid string?

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).

Should I create a new DateFormatter for each call?

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.

How is DateFormatter different from RelativeDateFormatter?

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.

How to handle dates without time zone from API?

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.

How to make DateFormatter thread-safe?

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

  • DateFormatter — a Foundation class for converting Date to string and back with locale, time zone and calendar support.
  • Preset styles dateStyle and timeStyle with .short, .medium, .long, .full variants cover most UI scenarios.
  • Custom dateFormat via LDML symbols gives full control over format but requires caution with localization.
  • Locale and TimeZone — required properties for correct display: for UI — system locale, for API — en_US_POSIX and UTC.
  • ISO8601DateFormatter — the preferred choice for date serialization in API due to speed and stability.
  • DateFormatter is not thread-safe — use separate instances for each thread or synchronize access.
  • RelativeDateFormatter (iOS 15+) — the optimal solution for displaying relative dates in news feeds and messengers.

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.

Discuss the project

Read also