1. Время ≠ строка
Если вы когда-нибудь писали print(Date()) и видели там что-то вроде 2026-01-16 20:31:12 +0000, то вы уже сталкивались с главной проблемой: техническое представление даты не обязано быть удобным человеку. Человеку хочется «16 января 2026, 12:30», а программе хочется стабильности: чтобы строка всегда читалась одинаково и не ломалась из‑за языка системы.
DateFormatter — это переводчик между двумя мирами:
- мир данных: Date (момент времени);
- мир текста: String (то, что мы показываем или читаем).
Технически это тип из Foundation (а исторически — Swift-обёртка над NSDateFormatter, который «потерял NS‑префикс» в современном Swift). Поэтому почти в любом примере вам понадобится import Foundation.
Схема простая (и полезно держать её в голове, чтобы не пытаться «сравнивать строки дат»):
flowchart LR
A["Date (момент)"] -->|"DateFormatter.string(from:)"| B["String (человек видит)"]
C["String (пользователь ввёл)"] -->|"DateFormatter.date(from:)"| D["Date (момент)"]
Форматирование и парсинг
У DateFormatter есть два основных метода, и они зеркальны:
- string(from: Date) -> String — форматирование (Date → String)
- date(from: String) -> Date? — парсинг (String → Date)
Обратите внимание на маленькую, но очень важную деталь: при парсинге возвращается Date?, то есть Optional. Это честное признание API: строка может быть кривой, формат может не совпасть, локаль может быть другая — и вместо падения программа просто скажет: «Не смог распознать».
Начнём с самых маленьких примеров — буквально «подержать в руках» API.
import Foundation
let now = Date()
let f = DateFormatter()
f.dateStyle = .medium
f.timeStyle = .short
let text = f.string(from: now)
print(text) // Примерно: "Jan 16, 2026 at 12:30 PM" (зависит от окружения)
И обратное направление (с проверкой nil):
import Foundation
let f = DateFormatter()
f.dateFormat = "yyyy-MM-dd"
let input = "2026-01-16"
let parsed = f.date(from: input)
print(parsed as Any) // Optional(2026-01-16 00:00:00 +0000) или nil
Пока выглядит легко. А теперь важное уточнение: без настроек форматтер почти всегда бесполезен. Настройки — это половина смысла.
2. Стильный вывод: dateStyle и timeStyle
Когда вы делаете приложение для людей, вы чаще всего хотите, чтобы дата выглядела привычно для пользователя: на русском — «16 янв. 2026 г.», в США — «Jan 16, 2026», в Японии — по‑другому. Именно для этого у DateFormatter есть «режим стилей»: dateStyle и timeStyle.
Это удобно тем, что вы не думаете про токены формата (типа yyyy/MM), а просто выбираете уровень подробности. Это похоже на настройку «крупность помола» у кофе: вы говорите «мелко/средне/крупно», а не описываете каждую крупинку.
Вот небольшой ориентир:
| Стиль | Пример смысла | Когда уместно |
|---|---|---|
|
коротко | списки, таблицы, компактный вывод |
|
нормально читаемо | большинство UI/сообщений |
|
подробно | отчёты, письма, «для красоты» |
|
максимально подробно | редко, когда прям надо «официально» |
Пример:
import Foundation
let f = DateFormatter()
f.dateStyle = .long
f.timeStyle = .short
print(f.string(from: Date())) // зависит от локали системы
И здесь сразу появляется нюанс: результат зависит от окружения. На вашей машине может быть одна локаль и часовой пояс, а у пользователя — другая. В «человеческом» режиме это нормально: вы как раз этого и хотите.
Но если вы хотите стабильную техническую строку, то стилей мало.
3. Фиксированный формат: dateFormat и дисциплина
Когда вы делаете формат для данных (например, пользователь вводит дату в заданном виде, или вы сохраняете дату как строку в файле), вам нужен фиксированный шаблон. Для этого используется dateFormat.
Идея такая: вы задаёте строку-шаблон, а DateFormatter по ней либо строит строку, либо пытается её распарсить.
Пример «технической» даты без локальных слов:
import Foundation
let f = DateFormatter()
f.dateFormat = "yyyy-MM-dd HH:mm"
let now = Date()
print(f.string(from: now)) // например: "2026-01-16 12:30"
Мини-таблица самых популярных токенов (только самые базовые — без «тонких ловушек», они будут в следующей лекции дня):
| Токен | Значение | Пример |
|---|---|---|
|
год | |
|
месяц (01–12) | |
|
день месяца (01–31) | |
|
часы (00–23) | |
|
минуты (00–59) | |
Важно привыкнуть к мысли: регистр букв — часть смысла. MM и mm — это разные вещи. Да, это похоже на то, что мир дат решил немного потроллить человечество. Но так исторически сложилось.
4. Локаль: Locale и эффект «работало у меня»
Представьте, что вы парсите строку 16 Jan 2026. На английской локали это читается. На русской — может перестать. А если вы парсите 16 янв. 2026, то наоборот.
Locale — это набор правил «как в этом языке/регионе принято писать даты, месяцы, разделители, AM/PM и т.д.». Если вы используете dateStyle/timeStyle, локаль особенно важна, потому что форматтер пытается быть «естественным» для пользователя.
Когда вы парсите технический формат, вы почти всегда хотите предсказуемость. Классический практический приём: ставить фиксированную локаль "en_US_POSIX". Это такая «нейтральная» локаль, которая помогает стабильно парсить технические строки (особенно если в формате есть названия месяцев, AM/PM и подобное).
Пример: парсим английскую строку месяца, но принудительно задаём локаль.
import Foundation
let f = DateFormatter()
f.locale = Locale(identifier: "en_US_POSIX")
f.dateFormat = "dd MMM yyyy"
let d = f.date(from: "16 Jan 2026")
print(d as Any) // Optional(...)
А теперь важный вывод на уровне привычки: если у вас формат фиксирован, не полагайтесь на локаль по умолчанию. По умолчанию она берётся из системы, а это значит — «в разных местах мира по‑разному».
5. Таймзона: TimeZone и разное отображение одного Date
Теперь ещё один «слой реальности», который ломает мозг новичкам: Date — это момент. Но когда вы превращаете момент в строку, вам надо решить: в какой таймзоне показать время?
Если вы не задаёте timeZone, форматтер обычно использует таймзону устройства. Это логично для UI, но может быть неожиданно для данных.
Сначала пример: создадим «полдень UTC» и покажем его в двух таймзонах.
import Foundation
let calendar = Calendar(identifier: .gregorian)
var comps = DateComponents()
comps.year = 2026
comps.month = 1
comps.day = 16
comps.hour = 12
comps.minute = 0
comps.timeZone = TimeZone(secondsFromGMT: 0)
let date = calendar.date(from: comps) ?? Date()
let f = DateFormatter()
f.dateFormat = "yyyy-MM-dd HH:mm ZZZZ"
f.timeZone = TimeZone(secondsFromGMT: 0)
print(f.string(from: date)) // "2026-01-16 12:00 +0000"
f.timeZone = TimeZone(secondsFromGMT: -8 * 3600)
print(f.string(from: date)) // "2026-01-16 04:00 -0800"
Важная мысль: date один и тот же, а строки разные — потому что «локальное время» зависит от таймзоны. Это не баг и не «Swift опять сломался». Это нормальная математика времени.
Практическое правило:
- если вы делаете строку «для человека на его устройстве», часто можно оставить таймзону по умолчанию;
- если вы делаете строку «как данные / для протокола / для одинакового поведения», задавайте таймзону явно (часто UTC).
6. Пример: мини‑планировщик чтения
Сейчас соберём маленькую «живую» историю, чтобы DateFormatter не был просто набором методов. Пусть у нас будет консольный планировщик чтения: пользователь вводит книги и дедлайны, а программа печатает список красивым текстом.
Мы не трогаем сложный CLI‑парсинг (он будет в следующих днях), а используем то, что у нас уже есть: readLine(), split, Optional, массивы и struct.
Модель данных: ReadingItem
Начнём с простого типа.
import Foundation
struct ReadingItem {
let title: String
let dueDate: Date
}
Здесь dueDate — именно Date, а не String. Строка — это «упаковка», а Date — нормальные данные. Это позволит нам сортировать, сравнивать, считать «сколько осталось» и т.д. (да, даже если пока мы это не делаем).
Форматтеры для ввода и вывода
Создавать DateFormatter() в каждом чихе — дорого и неудобно: легко случайно настроить его по‑разному в разных местах. Поэтому заведём один форматтер для ввода и один — для вывода.
import Foundation
let inputFormatter: DateFormatter = {
let f = DateFormatter()
f.locale = Locale(identifier: "en_US_POSIX")
f.timeZone = TimeZone(secondsFromGMT: 0)
f.dateFormat = "yyyy-MM-dd"
return f
}()
И форматтер для вывода «чуть более дружелюбно»:
import Foundation
let outputFormatter: DateFormatter = {
let f = DateFormatter()
f.dateStyle = .medium
f.timeStyle = .none
return f
}()
Обратите внимание: для ввода мы сделали фиксированный формат "yyyy-MM-dd" (и зафиксировали locale/timeZone, чтобы парсинг был предсказуем), а для вывода выбрали dateStyle = .medium, чтобы пользователь видел привычный вид на своём устройстве.
Парсинг даты из строки
Теперь напишем маленькую функцию: на вход String, на выход Date?.
import Foundation
func parseDueDate(_ text: String) -> Date? {
return inputFormatter.date(from: text)
}
Коротко, но здесь важна сама идея: раз парсинг может не получиться, мы честно возвращаем Optional.
Чтение элемента и валидация
Пусть пользователь вводит строку в формате:
Название книги | 2026-02-01
Просто и предсказуемо.
import Foundation
func readItem() -> ReadingItem? {
guard let line = readLine() else { return nil }
let parts = line.split(separator: "|").map { $0.trimmingCharacters(in: .whitespaces) }
guard parts.count == 2 else { return nil }
guard let date = parseDueDate(parts[1]) else { return nil }
return ReadingItem(title: parts[0], dueDate: date)
}
Если что-то не так — возвращаем nil. Это честно: «не смогли прочитать элемент».
Печать списка «по‑человечески»
Теперь выведем элементы так, чтобы читалось приятно.
import Foundation
func printItems(_ items: [ReadingItem]) {
for item in items {
let dateText = outputFormatter.string(from: item.dueDate)
print("- \(item.title) до \(dateText)")
}
}
На практике это будет что-то вроде: - Clean Code до Jan 16, 2026 (или по‑русски, если системная локаль русская).
И вот здесь становится видно, почему нам выгодно хранить Date, а не строку: вывод можно менять без переписывания данных.
Минимальная диагностика: если парсинг возвращает nil
Когда работа с датами «не работает», ощущения обычно такие: «ну он же должен распарсить, я же написал дату!». А DateFormatter молча возвращает nil. В этот момент полезно действовать как инженер, а не как шаман.
Первое: убедитесь, что формат совпадает с вводом. Если dateFormat = "yyyy-MM-dd", то строка должна быть ровно вида 2026-01-16, без времени.
Второе: убедитесь, что вы не парсите «человеческий формат» техническим форматтером. Например, Jan 16, 2026 не распарсится форматтером "yyyy-MM-dd". Это нормально.
Третье: если в строке есть слова (название месяца), фиксируйте Locale(identifier: "en_US_POSIX") (или другую подходящую), иначе Jan в русской локали может стать проблемой.
Четвёртое: если вы работаете с датой на границе суток (00:00) и внезапно «съехал день», проверьте TimeZone. Даже если мы сегодня не уходим в совсем тонкие кейсы, таймзона — первая вещь, которую стоит подозревать.
7. Типичные ошибки при работе с DateFormatter
Ошибка №1: сравнивать строки дат вместо Date.
Строка — это картинка, а не данные. Сегодня у вас формат 16.01.2026, завтра Jan 16, 2026, и сравнение строк начнёт давать странные результаты. Сравнивать и сортировать нужно Date, а строку получать уже в момент вывода.
Ошибка №2: ожидать, что date(from:) всегда распарсит.
Парсинг возвращает Date? не из вредности, а потому что строка может не совпасть с форматом. Если вы делаете inputFormatter.date(from: text)!, то вы подписываете контракт «строка всегда валидна». В учебных примерах так иногда делают, но в реальном коде это почти гарантированный краш.
Ошибка №3: использовать dateStyle/timeStyle для технических строк.
Стильный вывод удобен для пользователя, но он зависит от локали и настроек устройства. Если вы сохраняете такие строки в данные или передаёте между системами, вы создаёте себе проблему «у меня читалось, у него не читается». Для технических строк используйте фиксированный dateFormat и фиксируйте Locale.
Ошибка №4: не учитывать TimeZone и получать «съехавшее» время.
Один и тот же Date в UTC и в таймзоне пользователя будет выглядеть по-разному. Если вы парсите в одной таймзоне, а форматируете в другой (или полагаетесь на дефолт), можно внезапно увидеть другое число или другой час. Как минимум в тех местах, где важна точность, таймзону стоит задавать явно.
Ошибка №5: создавать DateFormatter() внутри цикла или в каждом вызове функции.
Форматтер — объект с настройками, и его создание не бесплатное. Кроме производительности, появляется риск, что вы случайно настроите разные форматтеры по‑разному и получите «плавающее» поведение. Гораздо спокойнее держать один форматтер (или пару: для ввода и вывода) и переиспользовать.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ