DateFormatter — клас Foundation, предназначен за двупосочно преобразуване между обекти Date и тяхното низово представяне. Класът взема предвид локала, часовата зона и календара на потребителя, осигурявайки правилно показване на дати в която и да е регион на света. Според Apple Developer Documentation (2025) DateFormatter подкрепа четири предварително определени стила на дата и час, както и пълност персонализирани формати чрез низ на шаблон. Без DateFormatter не е възможно да се покаже правилно датата на потребителя в интернационализирано приложение.
Основни
DateFormatter — клас от framework- а 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. UIDatePicker използва вътрешно DateFormatter за показване на дати в режим countDownTimer. TextField с форматер на входа може автоматично да валидира датата, въведена от потребителя. Core Data подкрепа атрибути от тип Date, но тяхното низово показване винаги се осъществява чрез DateFormatter.
Thread Safety — DateFormatter не е безопасен за нишки. Промяната на свойствата на форматера от различни нишки води до недефинирано поведение. За многонишкова употреба създавайте отделни инстанции на форматера за всяка нишка или използвайте синхронизация чрез NSLock или serial queue.
dateStyle и timeStyle — най-лесните начини за настройка на показването на дата. Всеки стил има четири варианта: .short, .medium, .long, .full. Комбинацията от dateStyle и timeStyle позволява независимо конфигуриране на формата на датата и часа, а свойството .none изключва съответната част.
За американския локал стил .short форматира датата като „7/21/26“, а за българския — като „21.07.2026 г.“. Стил .long за българския локал показва „21 юли 2026 г.“, а .full — „вторник, 21 юли 2026 г.“ с посочване на деня от седмицата. И четирите стила автоматично се адаптират към регионалните стандарти, включително поредицата на компонентите и разделителите.
RelativeDateFormatter в iOS 15+ предлага алтернативен подход чрез RelativeDateFormatter и DateIntervalFormatter. RelativeDateFormatter показва „днес“, „вчера“, „след 3 дни“ за текущия контекст. DateIntervalFormatter показва диапазони от дати: “21–25 юли 2026 г.” — за резервации и планиране.
| Стил | Пример (bg_BG) | Пример (en_US) |
|---|---|---|
| .short | 21.07.2026 г. | 7/21/26 |
| .medium | 21.07.2026 г. | Jul 21, 2026 |
| .long | 21 юли 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 позволява да се определи произволен шаблон за форматиране с използване на символите от спецификацията 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”) за фиксиран междурегионален формат, особено при анализа на дати от сървърни отговори.
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "ru_RU")
formatter.dateFormat = "d MMMM yyyy"
let customString = formatter.string(from: Date())
// "21 July 2026"
// Анализ на персонализиран низ
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 работят различно в различни локали.
Locale определя как се показват имената на месеците, дните от седмицата и какви разделители се използват. DateFormatter по подразбиране използва Locale.current, но в някои сценарии се изисква посочване на конкретен локал: за фиксиран формат в логовете използвайте en_US_POSIX, за сървърни дати — локал, идентичен на сървъра.
Свойството TimeZone определя часовата зона за показване. По подразбиране се използва системната часова зона, но за приложения с международна аудитория често се изисква показване на датите в часовата зона на потребителя или в UTC. Промяната на timeZone засяга само показването — стойността Date остава непроменена.
Важна характеристика: ако DateFormatter се използва за анализ на низ и низът съдържа посочване на часова зона (напр. “2026-07-21T14:30:00Z” с Z за UTC), свойството timeZone се игнорира — форматерът използва часовата зона от низа. Ако в низа липсва часова зона, се прилага timeZone на форматера.
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"
// Анализ без часова зона в низа
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 — специализиран форматер за работа с дати в формат 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. Изборът на стратегия засяга цялия животен цикъл на сериализацията и трябва да бъде единен за всички API ендпойнти.
// ISO8601DateFormatter
let isoFormatter = ISO8601DateFormatter()
isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let isoString = isoFormatter.string(from: Date())
// "2026-07-21T14:30:00.000Z"
// JSONEncoder с ISO8601
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
// Алтернатива: JSONEncoder с персонализиран форматер
let customEncoder = JSONEncoder()
customEncoder.dateEncodingStrategy = .formatted(myFormatter)
DateFormatter vs ISO8601DateFormatter — избирайте ISO8601DateFormatter за сериализация и анализ на дати в API, тъй като работи 5-10 пъти по-бързо от DateFormatter и не е подложен на грешки от локализация. DateFormatter оставете за потребителския интерфейс, където се изисква локализирано показване с имената на месеците и дните от седмицата на родния език на потребителя.
Нека разгледаме реални сценарии за използване на DateFormatter в iOS приложение: показване в списък с новини, въвеждане на дата на раждане и експорт на отчет с дати в различни часови зони.
RelativeDateFormatter е оптимален за новинарски канали. Той показва „токако“, „преди 5 минути“, „вчера“ за пресни новини и преминава към пълна дата за старите. Прагът на преключване се настроява чрез calendar: за новини използвайте праг от 24 часа, за месенджите — една седмица.
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 г.“ и локал „bg_BG“. При анализ на въведения низ е важно да се обработят възможните грешки: форматерът върща nil за невалиден низ. След успешен анализ датата се проверява дали е в позволения диапазон — не преди 1900 г., не след днешна дата.
Експорт на отчет с дати изисква фиксиран формат, независен от локала на потребителя. Използвайте dateFormat „yyyy-MM-dd HH:mm:ss“ с локал en_US_POSIX и часова зона UTC. Този подход гарантира, че файлът ще се отвори правилно в която и да е държава, независимо от регионалните настройки на системата.
| Сценарий | Форматер | Ключова настройка |
|---|---|---|
| Новинарски канал | RelativeDateFormatter | unitsStyle = .full |
| Въвеждане на дата | DateFormatter | dateFormat + fallback |
| API сериализация | ISO8601DateFormatter | withInternetDateTime |
| Експорт на отчет | DateFormatter | en_US_POSIX + UTC |
Често задавани въпроси
Най-честата причина — несъвпадение на dateFormat с формата на низа. Например, форматът „dd.MM.yyyy г.“ няма да анализира низа „2026-07-21“. Втората причина — несъвпадение на локала: низът „July 21, 2026“ няма да се анализира с локал bg_BG. Трета — печатни грешки в символите на LDML: използвайте yyyy, а не YYYY (различно значение).
Не. DateFormatter е тежък обект, инициализацията му включва зареждане на данни за локала. Създавайте един инстанс за тип форматиране и го преизползвайте. В многонишкова среда използвайте Thread-local storage или пул от форматери с serial queue за синхронизация.
DateFormatter показва абсолютна дата (21 юли 2026 г.), а RelativeDateFormatter — относителна (днес, вчера, след 3 дни). RelativeDateFormatter се появява в iOS 15+ и използва същия LDML шаблон, но автоматично избира относително показване.
Задайте timeZone на форматера на UTC преди анализ. Ако сървърът върща дата в локално време без посочване на зона, проверете спецификацията на API — най-вероятно се подразбира UTC. За ISO 8601 с Z на края timeZone не е необходим — форматерът анализира изместването от низа.
Не използвайте един инстанс от различни нишки без синхронизация. Създавайте нов инстанс в всяка нишка или използвайте Thread.current.threadDictionary за съхранение. Алтернатива — NSLock с заключване по време на string(from:) и date(from:).
Обобщение
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също