🍎 Swift

Trabalhando com data e hora em Swift: um guia completo

Что это такое и зачем? (аналогия из реальной жизни)

Представьте, что вы ведёте дневник. Каждый день вы записываете: «Сегодня 15 марта, 14:30, я пошёл гулять». Через месяц вы хотите узнать: «А сколько дней прошло с той прогулки?» или «В какой день недели это было?». Без даты и времени ответить на эти вопросы было бы невозможно.

В программировании работа с датой и временем — это умение компьютера понимать, хранить и вычислять моменты времени. Компьютер не чувствует времени, как человек. Для него время — это просто число (количество секунд, прошедших с 1 января 2001 года по Гринвичу).

Зачем это нужно в реальных приложениях?

  • Календари и напоминания — чтобы будильник сработал ровно в 7:00.
  • Банковские операции — чтобы знать, когда был сделан перевод.
  • Социальные сети — чтобы показать: «Опубликовано 5 минут назад».
  • Игры — чтобы отсчитывать время до окончания бонуса.
  • Фитнес-трекеры — чтобы записать, сколько вы бегали сегодня.
Важно: Время в программировании — это не «строка» (например, «2024-03-15»), а специальный объект, с которым можно производить математические операции: складывать, вычитать, сравнивать.

Теория: как это работает изнутри

В Swift работа с датой и временем построена на трёх китах:

1. Date — «Момент во времени»

Это самый главный тип. Date хранит одно единственное число — количество секунд, прошедших с 1 января 2001 года, 00:00:00 UTC (эта точка называется «reference date»).

Пример: если вы создадите Date() прямо сейчас, внутри него будет число вроде 735_000_000.0 (секунд). Это число не зависит от того, в какой стране вы находитесь.

2. TimeZone — «Часовой пояс»

Земля разделена на 24 часовых пояса. Когда в Москве 12:00, в Нью-Йорке ещё 4:00 утра. TimeZone хранит смещение относительно UTC (Всемирного координированного времени).

Москва — это UTC+3 (зимой) или UTC+4 (летом).

3. Calendar — «Календарь»

Люди привыкли к дням, месяцам и годам. Но компьютер не знает, что в январе 31 день, а в феврале — 28 (или 29). Calendar — это «переводчик» между числом секунд (Date) и человеческими единицами (день, месяц, год).

В Swift используется григорианский календарь (тот, которым мы пользуемся в повседневной жизни).

Как они работают вместе?

Представьте конвейер:

  1. У вас есть Date — число секунд (например, 735_000_000).
  2. Вы берёте Calendar и говорите: «Разложи это число на день, месяц, год по московскому времени».
  3. Calendar использует TimeZone, чтобы понять, какое сейчас время в Москве.
  4. На выходе вы получаете: «15 марта 2024 года, 14:30:00».
Запомните: Date — это «сырое» число. Calendar + TimeZone — это «очки», через которые мы смотрим на это число и видим привычные нам даты.

Синтаксис в Swift: все формы и варианты

В Swift есть несколько способов работать с датой и временем. Рассмотрим основные классы и структуры.

Основные типы

Тип Описание Пример
Date Момент времени (количество секунд от reference date) let now = Date()
Calendar Календарь (григорианский, исламский и т.д.) let cal = Calendar.current
TimeZone Часовой пояс let tz = TimeZone.current
DateComponents Набор чисел: год, месяц, день, час, минута, секунда DateComponents(year: 2024, month: 3, day: 15)
DateFormatter Преобразует Date в строку и обратно DateFormatter()
DateInterval Промежуток между двумя датами DateInterval(start: d1, end: d2)

Создание Date

// 1. Текущий момент
let now = Date()
print(now) // Выведет что-то вроде "2024-03-15 11:30:00 +0000"

// 2. Конкретная дата через DateComponents
var components = DateComponents()
components.year = 2024
components.month = 12
components.day = 25
components.hour = 10
components.minute = 30

let calendar = Calendar.current
let christmas = calendar.date(from: components)!
print(christmas) // 2024-12-25 07:30:00 +0000 (если вы в UTC+3)

// 3. Через строку (DateFormatter)
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm"
let dateFromString = formatter.date(from: "2024-06-01 12:00")!
print(dateFromString) // 2024-06-01 09:00:00 +0000 (если вы в UTC+3)

Извлечение компонентов из Date

let now = Date()
let calendar = Calendar.current

// Получаем отдельные части даты
let year = calendar.component(.year, from: now)
let month = calendar.component(.month, from: now)
let day = calendar.component(.day, from: now)
let hour = calendar.component(.hour, from: now)
let minute = calendar.component(.minute, from: now)
let weekday = calendar.component(.weekday, from: now) // 1 = воскресенье, 2 = понедельник...

print("Сегодня (day).(month).(year), время (hour):(minute)")

Арифметика с датами

let now = Date()
let calendar = Calendar.current

// Прибавляем 7 дней
let nextWeek = calendar.date(byAdding: .day, value: 7, to: now)!

// Прибавляем 2 часа
let twoHoursLater = calendar.date(byAdding: .hour, value: 2, to: now)!

// Разница между датами
let start = Date()
sleep(2) // ждём 2 секунды
let end = Date()
let difference = end.timeIntervalSince(start) // 2.0 (секунды)

Форматирование даты в строку

let now = Date()
let formatter = DateFormatter()

// Русская локаль
formatter.locale = Locale(identifier: "ru_RU")

// Разные стили
formatter.dateStyle = .full
formatter.timeStyle = .full
print(formatter.string(from: now)) // "пятница, 15 марта 2024 г., 14:30:00 Московское стандартное время"

formatter.dateStyle = .medium
formatter.timeStyle = .short
print(formatter.string(from: now)) // "15 мар. 2024 г., 14:30"

// Свой формат
formatter.dateFormat = "dd.MM.yyyy HH:mm"
print(formatter.string(from: now)) // "15.03.2024 14:30"

Сравнение дат

let date1 = Date()
let date2 = date1.addingTimeInterval(3600) // +1 час

if date1 < date2 {
    print("date1 раньше, чем date2")
}

if date1 == date2 {
    print("даты равны")
}

// Проверка: одна дата в том же дне, что и другая?
let calendar = Calendar.current
if calendar.isDate(date1, inSameDayAs: date2) {
    print("Это один и тот же день")
}

Примеры кода — от простого к сложному

Пример 1: «Который час?»

// Самый простой пример: выводим текущее время
import Foundation

let now = Date()
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss"
print("Сейчас (formatter.string(from: now))")
// Вывод: "Сейчас 14:30:45"

Пример 2: «Сколько дней до Нового года?»

import Foundation

let now = Date()
let calendar = Calendar.current

// Создаём дату Нового года (1 января следующего года)
var newYearComponents = DateComponents()
newYearComponents.year = calendar.component(.year, from: now) + 1
newYearComponents.month = 1
newYearComponents.day = 1

let newYear = calendar.date(from: newYearComponents)!

// Вычисляем разницу в днях
let daysUntilNewYear = calendar.dateComponents([.day], from: now, to: newYear).day!
print("До Нового года осталось (daysUntilNewYear) дней!")
// Вывод: "До Нового года осталось 291 дней!"

Пример 3: «Проверка: сегодня выходной?»

import Foundation

let now = Date()
let calendar = Calendar.current
let weekday = calendar.component(.weekday, from: now)

// В России выходные — суббота (7) и воскресенье (1)
if weekday == 1 || weekday == 7 {
    print("Ура, сегодня выходной!")
} else {
    print("Сегодня рабочий день.")
}

Пример 4: «Таймер обратного отсчёта»

import Foundation

let targetDate = Date().addingTimeInterval(120) // 2 минуты от текущего момента

// Функция, которая показывает, сколько осталось
func timeRemaining(until date: Date) -> String {
    let now = Date()
    let interval = date.timeIntervalSince(now)
    
    guard interval > 0 else { return "Время вышло!" }
    
    let minutes = Int(interval) / 60
    let seconds = Int(interval) % 60
    return "Осталось (minutes) мин (seconds) сек"
}

print(timeRemaining(until: targetDate))
// Вывод: "Осталось 1 мин 59 сек"

Пример 5: «Календарь на месяц»

import Foundation

let calendar = Calendar.current
let now = Date()

// Получаем первый день текущего месяца
let components = calendar.dateComponents([.year, .month], from: now)
let firstDayOfMonth = calendar.date(from: components)!

// Получаем количество дней в месяце
let range = calendar.range(of: .day, in: .month, for: firstDayOfMonth)!
let numberOfDays = range.count

print("В текущем месяце (numberOfDays) дней")
// Вывод: "В текущем месяце 31 дней"

Пример 6: «Возраст пользователя»

import Foundation

let birthDateString = "2005-08-20"
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"

guard let birthDate = formatter.date(from: birthDateString) else {
    print("Неверный формат даты")
    exit(1)
}

let now = Date()
let calendar = Calendar.current

// Вычисляем возраст
let ageComponents = calendar.dateComponents([.year, .month, .day], from: birthDate, to: now)
let years = ageComponents.year!
let months = ageComponents.month!
let days = ageComponents.day!

print("Возраст: (years) лет, (months) месяцев, (days) дней")
// Вывод: "Возраст: 18 лет, 6 месяцев, 23 дней"

Пример 7: «Расписание занятий»

import Foundation

// Создаём расписание: занятие начинается в 10:00 и длится 1.5 часа
let calendar = Calendar.current
var startComponents = DateComponents()
startComponents.hour = 10
startComponents.minute = 0

let startTime = calendar.date(from: startComponents)!
let duration: TimeInterval = 90 * 60 // 90 минут в секундах
let endTime = startTime.addingTimeInterval(duration)

let formatter = DateFormatter()
formatter.dateFormat = "HH:mm"

print("Занятие: (formatter.string(from: startTime)) - (formatter.string(from: endTime))")
// Вывод: "Занятие: 10:00 - 11:30"

Пример 8: «Сложный: часовые пояса»

import Foundation

let now = Date()
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss"

// Время в Москве
formatter.timeZone = TimeZone(identifier: "Europe/Moscow")
let moscowTime = formatter.string(from: now)
print("Москва: (moscowTime)")

// Время в Нью-Йорке
formatter.timeZone = TimeZone(identifier: "America/New_York")
let newYorkTime = formatter.string(from: now)
print("Нью-Йорк: (newYorkTime)")

// Время в Токио
formatter.timeZone = TimeZone(identifier: "Asia/Tokyo")
let tokyoTime = formatter.string(from: now)
print("Токио: (tokyoTime)")

Сравнение с Python: чем похоже и чем отличается

Операция Swift Python
Текущая дата/время Date() datetime.now()
Создание из компонентов DateComponents + Calendar datetime(2024, 3, 15, 14, 30)
Разница между датами date1.timeIntervalSince(date2) date1 - date2 (возвращает timedelta)
Прибавление дней calendar.date(byAdding: .day, value: 7, to: date) date + timedelta(days=7)
Форматирование DateFormatter с dateFormat strftime("%Y-%m-%d")
Часовые пояса TimeZone(identifier:) pytz.timezone("Europe/Moscow")
Парсинг строки formatter.date(from: "2024-03-15") datetime.strptime("2024-03-15", "%Y-%m-%d")

Главные отличия:

  • Swift требует явного использования Calendar для любых операций с днями/месяцами. Python делает это автоматически.
  • Swift использует DateFormatter для преобразования в строку и обратно. В Python это делается методами strftime и strptime.
  • Swift более строгий: многие операции возвращают опциональные значения (?), потому что дата может быть некорректной.
  • Python проще для новичков, но Swift даёт больше контроля над часовыми поясами и календарями.

Типичные ошибки и подводные камни

Ошибка 1: Игнорирование часового пояса

// Ошибка: создаём дату без учёта часового пояса
var components = DateComponents()
components.year = 2024
components.month = 3
components.day = 15
components.hour = 12

let calendar = Calendar.current
let date = calendar.date(from: components)!
// Эта дата будет интерпретирована как 12:00 по Гринвичу, а не по вашему времени!

Решение: Всегда указывайте часовой пояс при создании даты из компонентов:

var components = DateComponents()
components.year = 2024
components.month = 3
components.day = 15
components.hour = 12
components.timeZone = TimeZone(identifier: "Europe/Moscow")

let calendar = Calendar.current
let date = calendar.date(from: components)!

Ошибка 2: Сравнение дат через ==

let date1 = Date()
let date2 = date1.addingTimeInterval(0.001) // +1 миллисекунда

if date1 == date2 {
    print("Равны") // Это не выполнится!
}

Решение: Используйте допуск при сравнении:

if abs(date1.timeIntervalSince(date2)) < 0.01 {
    print("Даты практически равны")
}

Ошибка 3: Неправильный формат DateFormatter

let formatter = DateFormatter()
formatter.dateFormat = "YYYY-MM-dd" // Ошибка! Должно быть "yyyy"

Пояснение: YYYY (большие буквы) — это «год недели», а yyyy (маленькие) — календарный год. В конце декабря они могут давать разные значения!

Ошибка 4: Забыли про Locale

let formatter = DateFormatter()
formatter.dateFormat = "MM/dd/yyyy"
// Если у пользователя локаль, где принят формат "dd.MM.yyyy", 
// то парсинг может сломаться

Решение: Всегда устанавливайте locale для DateFormatter:

formatter.locale = Locale(identifier: "en_US_POSIX") // для стандартного формата

Ошибка 5: Работа с датами в фоновом потоке

Date — потокобезопасный тип, но DateFormatter — нет. Создавайте новый DateFormatter для каждого потока или используйте os_unfair_lock.

Лучшие практики и советы профессионалов

  1. Храните даты в UTC. Всегда храните даты в серверной базе данных в UTC, а конвертируйте в локальное время только при отображении пользователю.
  2. Используйте ISO 8601. Для обмена датами между сервером и приложением используйте формат ISO 8601: 2024-03-15T14:30:00Z.
  3. Не создавайте DateFormatter каждый раз. Создание DateFormatter — дорогая операция. Создайте его один раз и переиспользуйте.
  4. Используйте RelativeDateTimeFormatter. Для отображения «только что», «5 минут назад», «вчера» используйте встроенный RelativeDateTimeFormatter.
  5. Проверяйте граничные случаи. Что будет, если пользователь ввёл 29 февраля 2023 года? (2023 — не високосный). Всегда проверяйте, что дата корректна.
  6. Используйте Calendar.current с осторожностью. Пользователь может использовать не григорианский календарь. Если вам нужен именно григорианский, создавайте его явно: Calendar(identifier: .gregorian).
  7. Для длительных операций используйте DispatchQueue. Если вы обрабатываете много дат (например, парсите CSV-файл с 10 000 дат), делайте это в фоновом потоке.
Совет профи: В Swift 5.5+ появился новый фреймворк SwiftDate (не путать со сторонней библиотекой). Он использует макросы и делает работу с датами ещё проще. Изучите его после освоения основ.

Вопросы для самопроверки

  1. Вопрос: Что хранит внутри себя объект Date?

    Ответ: Количество секунд, прошедших с 1 января 2001 года, 00:00:00 UTC.

  2. Вопрос: Зачем нужен Calendar при работе с датами?

    Ответ: Чтобы преобразовывать «сырое» число секунд (Date) в человеческие единицы (год, месяц, день) и обратно, а также для выполнения календарных вычислений (прибавление дней, месяцев).

  3. Вопрос: Какой метод используется для получения разницы между двумя датами в секундах?

    Ответ: date1.timeIntervalSince(date2).

  4. Вопрос: В чём разница между yyyy и YYYY в DateFormatter?

    Ответ: yyyy — календарный год, YYYY — «год недели» (week year). В конце декабря они могут давать разные значения.

  5. Вопрос: Как создать дату «25 декабря 2024 года, 10:00» с учётом московского времени?

    Ответ: Использовать DateComponents с указанием timeZone и Calendar.

  6. Вопрос: Почему не стоит сравнивать даты через ==?

    Ответ: Потому что Date хранит время с точностью до наносекунды. Две даты, созданные с разницей в миллисекунду, не будут равны.

  7. Вопрос: Какой класс используется для отображения относительного времени («5 минут назад»)?

    Ответ: RelativeDateTimeFormatter.

  8. Вопрос: Что произойдёт, если попытаться создать дату 29 февраля 2023 года?

    Ответ: calendar.date(from: components) вернёт nil, потому что 2023 год не високосный.

  9. Вопрос: Как получить номер дня недели для текущей даты?

    Ответ: calendar.component(.weekday, from: Date()). Возвращает 1 для воскресенья, 2 для понедельника и т.д.

  10. Вопрос: В чём отличие работы с датами в Swift и Python?

    Ответ: В Swift нужно явно использовать Calendar для операций с днями/месяцами, а в Python это делается автоматически через timedelta. Swift более строгий и требует обработки опциональных значений.

Итог и что изучить дальше

Мы прошли большой путь: от понимания того, что такое дата и время в программировании, до написания сложных примеров с часовыми поясами и календарём.

Ключевые выводы:

  • Date — это просто число секунд.
  • Calendar и TimeZone — это «очки», через которые мы смотрим на это число.
  • DateFormatter нужен для общения с пользователем (преобразование в строку и обратно).
  • Всегда учитывайте часовой пояс и локаль пользователя.
  • Проверяйте граничные случаи (високосные годы, 29 февраля, переход на летнее время).

Что изучить дальше:

  1. RelativeDateTimeFormatter — для отображения «только что», «через 2 дня».
  2. DateIntervalFormatter — для отображения промежутков («10:00 — 11:30»).
  3. ISO8601DateFormatter — для работы с форматом ISO 8601.
  4. SwiftDate (макросы) — новый способ работы с датами в Swift 5.9+.
  5. Работа с сетевыми запросами — как получать даты с сервера и синхронизировать их.
  6. UserDefaults и Codable — как сохранять даты в память устройства.
Напутствие: Не бойтесь экспериментировать с датами. Создайте свой проект — например, приложение-календарь или таймер обратного отсчёта до дня рождения. Практика — лучший способ запомнить все тонкости работы с датой и временем в Swift.