DateFormatter: ключевые понятия, форматирование даты и локализация

Автор: IT Sectr Опубликовано: 2026-07-12 Время чтения: 7 мин

DateFormatter — это класс Foundation, предназначенный для двунаправленного преобразования между объектами Date и их строковым представлением. Класс учитывает локаль, часовой пояс и календарь пользователя, обеспечивая корректное отображение дат в любом регионе мира. По данным Apple Developer Documentation (2025), DateFormatter поддерживает четыре предустановленных стиля даты и времени, а также полностью кастомные форматы через строку шаблона. Без DateFormatter невозможно корректно отобразить дату пользователю в интернационализированном приложении.

Главное

  • DateFormatter — класс для преобразования Date в строку и обратно с учётом локали и часового пояса.
  • dateStyle и timeStyle — предустановленные стили (.short, .medium, .long, .full) для быстрого форматирования.
  • dateFormat — строка шаблона для кастомного формата, задаваемая через символы спецификации Unicode LDML.
  • Locale и TimeZone — свойства форматтера, определяющие региональное отображение и часовой пояс.
  • ISO8601DateFormatter — более быстрая альтернатива для формата ISO 8601 при сериализации в API.

Что такое DateFormatter?

DateFormatter — это класс из фреймворка Foundation, реализующий двунаправленное преобразование между Date и строкой. Он появился в OpenStep как NSDateFormatter и с тех пор остаётся основным инструментом форматирования дат во всех платформах Apple. Класс наследуется от Formatter и предоставляет удобный API для локализованного отображения дат.

Принцип работы DateFormatter основан на шаблонах Unicode LDML — тех же, что используются в ICU (International Components for Unicode). Шаблон задаётся через свойство dateFormat, где символы y, M, d, H, m, s соответствуют году, месяцу, дню, часам, минутам, секундам. Повторение символа определяет формат: "y" — двухзначный год, "yyyy" — четырёхзначный.

Создание DateFormatter — дорогая операция, так как при инициализации загружаются данные локали и календаря. Apple рекомендует создавать форматтер один раз для каждого типа форматирования и переиспользовать его. В SwiftUI и UIKit форматтеры часто кэшируются в статических свойствах или создаются лениво при первом обращении.

DateFormatter в iOS SDK

DateFormatter применяется во многих системных компонентах iOS. UIDatePicker использует DateFormatter внутри себя для отображения дат в режиме countDownTimer. TextField с форматером на входе может автоматически валидировать вводимую пользователем дату. Core Data поддерживает атрибуты типа Date, но их строковое отображение всегда выполняется через DateFormatter.

Thread Safety — DateFormatter не потокобезопасен. Модификация свойств форматтера из разных потоков приводит к неопределённому поведению. Для многопоточного использования создавайте отдельные экземпляры форматтера для каждого потока или используйте синхронизацию через NSLock или serial queue.

Стили форматирования DateFormatter

dateStyle и timeStyle — самые простые способы настроить отображение даты. Каждый стиль имеет четыре варианта: .short, .medium, .long, .full. Комбинация dateStyle и timeStyle позволяет независимо настроить формат даты и времени, а свойство .none отключает соответствующую часть.

Для американской локали .short форматирует дату как "7/21/26", а для русской — как "21.07.2026". Стиль .long для русской локали выводит "21 июля 2026 г.", а .full — "вторник, 21 июля 2026 г." с указанием дня недели. Все четыре стиля автоматически адаптируются под региональные стандарты, включая порядок компонентов и разделители.

SFDateFormatter в iOS 15+ предоставляет альтернативный подход через RelativeDateFormatter и DateIntervalFormatter. RelativeDateFormatter выводит "сегодня", "вчера", "через 3 дня" для оперативного контекста. DateIntervalFormatter отображает диапазоны дат: "21–25 июля 2026 г." — для бронирований и планирования.

СтильПример (ru_RU)Пример (en_US)
.short21.07.20267/21/26
.medium21 июл. 2026 г.Jul 21, 2026
.long21 июля 2026 г.July 21, 2026
.fullвторник, 21 июля 2026 г.Tuesday, July 21, 2026

При комбинировании стилей DateFormatter автоматически выбирает разделитель: для .short.date + .short.time результат может быть "21.07.2026, 14:30". Для .full.date + .full.time — "вторник, 21 июля 2026 г., 14:30:00 MSK". Разделителем управляет локаль, а не разработчик — это гарантирует соответствие региональным ожиданиям пользователя.

Кастомные форматы через dateFormat

dateFormat позволяет задать произвольный шаблон форматирования с использованием символов спецификации Unicode LDML. Это даёт полный контроль над отображением: можно вывести только год и месяц, или день недели без числа, или время без секунд. Кастомный формат незаменим для специфических требований дизайна.

Основные символы — yyyy (год: 2026), MM (месяц: 07), dd (день: 21), HH (часы: 14), mm (минуты: 30), ss (секунды: 00). Для полного названия месяца используйте MMMM (июль), для сокращённого — MMM (июл.). День недели — EEEE (вторник), сокращённый — E (вт).

При использовании dateFormat важно установить locale форматтера. Если locale не задано, форматтер использует системную локаль, что может быть нежелательно для фиксированного формата в API. Apple рекомендует устанавливать locale = Locale(identifier: "en_US_POSIX") для фиксированного межрегионального формата, особенно при парсинге дат из серверных ответов.

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 kastomnoy stroki
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
let date = formatter.date(from: "2026-07-21 14:30:00")!

Ошибка в dateFormat — одна из частых причин краша приложения. Если формат не совпадает со строкой, метод date(from:) возвращает nil. Используйте guard let или ?? для безопасного извлечения опционального значения. Для валидации формата тестируйте на всех поддерживаемых языках — некоторые символы LDML работают по-разному в разных локалях.

Локализация и TimeZone

Locale определяет, как отображаются названия месяцев, дней недели и какие разделители используются. DateFormatter по умолчанию использует Locale.current, но в некоторых сценариях требуется указать конкретную локаль: для фиксированного формата в логах используйте en_US_POSIX, для серверных дат — локаль идентичную серверной.

Свойство TimeZone определяет часовой пояс для отображения. По умолчанию используется системный часовой пояс, но для приложений с международной аудиторией часто требуется отображать даты в часовом поясе пользователя или в UTC. Изменение timeZone влияет только на отображение — значение Date остаётся неизменным.

Важная особенность: если DateFormatter используется для парсинга строки, а в строке содержится указание часового пояса (например, "2026-07-21T14:30:00Z" с Z для UTC), свойство timeZone игнорируется — форматтер использует часовой пояс из строки. Если часовой пояс в строке отсутствует, применяется timeZone форматтера.

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 bez timezone v stroke
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd HH:mm"
let utcDate = formatter.date(from: "2026-07-21 10:30")!

AutoupdatingCurrentLocale — специальный тип локали, которая автоматически обновляется при изменении системных настроек пользователя. DateFormatter поддерживает его по умолчанию. Если приложение работает в фоне и пользователь меняет язык системы, форматтер, созданный до изменения, продолжит использовать старую локаль — для обновления нужно создать новый экземпляр.

ISO8601DateFormatter и альтернативы

ISO8601DateFormatter — специализированный форматтер для работы с датами в формате ISO 8601. Этот формат является стандартом де-факто для REST API, JSON и обмена данными. ISO8601DateFormatter работает значительно быстрее DateFormatter, поскольку не зависит от локали и использует фиксированную грамматику парсинга.

Основные опции форматтера — .withInternetDateTime (2026-07-21T14:30:00Z), .withFractionalSeconds (добавляет миллисекунды), .withTimeZone (включает смещение часового пояса). Комбинируя опции, можно получить любой ISO 8601 вариант: с миллисекундами, с часовым поясом, с датой без времени.

JSONEncoder.DateEncodingStrategy позволяет глобально настроить кодирование дат для всех Codable моделей. Варианты — .iso8601 (использует ISO8601DateFormatter), .formatted(DateFormatter), .millisecondsSince1970, .secondsSince1970. Выбор стратегии влияет на весь lifecycle сериализации и должен быть единым для всех эндпоинтов API.

swift
// ISO8601DateFormatter
let isoFormatter = ISO8601DateFormatter()
isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let isoString = isoFormatter.string(from: Date())
// "2026-07-21T14:30:00.000Z"

// JSONEncoder s ISO8601
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601

// Alternative: JSONEncoder with custom formatter
let customEncoder = JSONEncoder()
customEncoder.dateEncodingStrategy = .formatted(myFormatter)

DateFormatter vs ISO8601DateFormatter — выбирайте ISO8601DateFormatter для сериализации и парсинга дат в API, так как он работает в 5-10 раз быстрее DateFormatter и не подвержен ошибкам локализации. DateFormatter оставляйте для пользовательского интерфейса, где требуется локализованное отображение с названиями месяцев и дней недели на родном языке пользователя.

Примеры форматирования дат

Рассмотрим реальные сценарии использования DateFormatter в iOS-приложении: отображение в списке новостей, ввод даты рождения и экспорт отчёта с датами в разных часовых поясах.

Отображение даты новости в списке

RelativeDateFormatter оптимален для новостных лент. Он выводит "только что", "5 минут назад", "вчера" для свежих новостей и переключается на полную дату для старых. Порог переключения настраивается через calendar: для новостей используйте порог в 24 часа, для мессенджеров — неделю.

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

Ввод даты рождения — ещё один частый сценарий. DateFormatter настраивается с конкретным dateFormat "dd.MM.yyyy" и locale "ru_RU". При парсинге введённой строки важно обработать возможные ошибки: форматтер возвращает nil для некорректной строки. После успешного парсинга дата проверяется на вхождение в допустимый диапазон — не раньше 1900 года, не позже сегодняшнего дня.

Экспорт отчёта с датами требует фиксированного формата, не зависящего от локали пользователя. Используйте dateFormat "yyyy-MM-dd HH:mm:ss" с локалью en_US_POSIX и часовым поясом UTC. Такой подход гарантирует, что файл откроется корректно в любой стране независимо от региональных настроек системы.

СценарийФорматтерКлючевая настройка
Новостная лентаRelativeDateFormatterunitsStyle = .full
Ввод датыDateFormatterdateFormat + fallback
API сериализацияISO8601DateFormatterwithInternetDateTime
Экспорт отчётаDateFormatteren_US_POSIX + UTC

Часто задаваемые вопросы

Почему DateFormatter возвращает nil для корректной строки?

Самая частая причина — несовпадение dateFormat с форматом строки. Например, формат "dd.MM.yyyy" не распарсит строку "2026-07-21". Вторая причина — несоответствие локали: строка "July 21, 2026" не распарсится с локалью ru_RU. Третья — опечатки в символах LDML: используйте yyyy, а не YYYY (разный смысл).

Нужно ли создавать новый DateFormatter для каждого вызова?

Нет. DateFormatter — тяжёлый объект, его инициализация включает загрузку данных локали. Создавайте один экземпляр на тип форматирования и переиспользуйте. В многопоточной среде используйте Thread-local storage или пул форматтеров с serial queue для синхронизации.

Чем DateFormatter отличается от RelativeDateFormatter?

DateFormatter выводит абсолютную дату (21 июля 2026 г.), а RelativeDateFormatter — относительную (сегодня, вчера, через 3 дня). RelativeDateFormatter появился в iOS 15+ и использует тот же LDML-шаблон, но автоматически подбирает относительное отображение.

Как обработать даты без часового пояса из API?

Установите timeZone форматтера в UTC перед парсингом. Если сервер отдаёт дату в локальном времени без указания таймзоны, уточните спецификацию API — вероятнее всего, подразумевается UTC. Для ISO 8601 с Z на конце timeZone не нужен — форматтер парсит смещение из строки.

Как сделать DateFormatter thread-safe?

Не используйте один экземпляр из разных потоков без синхронизации. Создавайте новый экземпляр в каждом потоке или используйте Thread.current.threadDictionary для хранения. Альтернатива — NSLock с блокировкой на время string(from:) и date(from:).

Итоги

  • DateFormatter — класс Foundation для преобразования Date в строку и обратно с учётом локали, часового пояса и календаря пользователя.
  • Предустановленные стили dateStyle и timeStyle с вариантами .short, .medium, .long, .full покрывают большинство UI-сценариев.
  • Кастомный dateFormat через символы LDML даёт полный контроль над форматом, но требует осторожности с локализацией.
  • Locale и TimeZone — обязательные свойства для корректного отображения: для UI — системная локаль, для API — en_US_POSIX и UTC.
  • ISO8601DateFormatter — предпочтительный выбор для сериализации дат в API благодаря скорости и стабильности.
  • DateFormatter не потокобезопасен — используйте отдельные экземпляры для каждого потока или синхронизируйте доступ.
  • RelativeDateFormatter (iOS 15+) — оптимальное решение для отображения относительных дат в новостных лентах и мессенджерах.

Мы разработаем мобильное приложение под ключ

IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

Читайте также