Новый API форматировщика дат в Swift

В версии Swift 5.5 и iOS 15 у нас появился новый API средства форматирования. С ним строковое отображение дат будет более декларативным и интуитивно понятным. Прежде чем переходить к его рассмотрению, напомним о том, как работает API средства форматирования. Сделаем это с помощью следующего примера:

extension DateFormatter {	
   static let MMddyy: DateFormatter = {
      let formatter = DateFormatter()
      formatter.timeZone = TimeZone(abbreviation: "UTC") //TimeZone.current
      formatter.dateFormat = "MM/dd/yy"
      return formatter
   }()
}

extension Date {
   func formatToString(using formatter: DateFormatter) -> String {
      return formatter.string(from: self)
   }
}

let date = Date()
print(date.formatToString(using: .MMddyy) // 07/18/2021

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

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

С новым API средства форматирования эта проблема решается: вместо настройки DateFormatter появляется возможность описать способ желаемого отображения дат. Теперь в нашем распоряжении четыре функции форматирования:

// 1.
func formatted() -> String

// 2.
func formatted<F>(_ format: F) 
  -> F.FormatOutput where F : FormatStyle, F.FormatInput == Date

// 3.
func ISO8601Format(_ style: Date.ISO8601FormatStyle = .init()) -> String

// 4.
func formatted(
  date: Date.FormatStyle.DateStyle, 
  time: Date.FormatStyle.TimeStyle) -> String

Начнем с самого простого примера, варианта номер один. В нем дата преобразуется в формат строки по умолчанию (дата + время):

let date = Date.now

print(date.formatted()) // 07/01/2021, 1:38 PM

Никаких преобразований здесь больше выполнить нельзя, и в каких-то случаях этого формата достаточно. Когда же формат даты надо настроить, приходится прибегнуть к варианту номер два:

func formatted<F>(_ format: F) 
   -> F.FormatOutput where F : FormatStyle, F.FormatInput == Date

Вначале этот вариант немного сложноват для понимания. Но становится проще, когда разберешься с тем, как он на самом деле используется.

Для задействования функции нужно указать FormatStyle в качестве параметра.

FormatStyle  —  это такой протокол, имеющий два связанных типа, input и output. В нашем случае input должен быть типом Date. К счастью, в Swift уже имеется структура Date.FormatStyle, соответствующая протоколу FormatStyle. Этот FormatStyle содержит статическую переменную dateTime: Date.FromatStyle, которая прямо задействуется в качестве параметра функции.

Теперь у нас есть экземпляр FormatStyle и методы экземпляра Date.FormatStyle для его настройки (их список смотрите в официальной документации). Приведем еще пару примеров:

let date = Date.now

var stringDate = 
   date.formatted(
      .dateTime
      .month(.wide)
      .day(.twoDigits)
      .year()
   )

print(stringDate) // July 01, 2021

date.formatted(
   .dateTime
   .month(.narrow)
   .day()
   .year(.twoDigits)
)

print(stringDate) // Jul 1, 21

Здесь берется переменная dateTime (это экземпляр Date.FromatStyle) и вызывается функция месяца, дня и года. Все три функции возвращают Date.FromatStyle. Это тот тип, которого ожидает параметр функции. Мы просто комбинируем возможные сочетания формата. Благодаря использованию этих методов экземпляра таких сочетаний предостаточно.

Обратите внимание: порядок, в котором функции вызываются, не влияет на конечный вывод. Вся тяжелая работа в Swift выполняется за нас, и корректный формат определяется, исходя из предпочтений пользователя.

Представим теперь, что нам нужно отправить на бэкенд строковое отображение даты в определенном формате? Например, в формате 2021–07–18. Для этого сценария придется использовать вариант номер три. Так же, как и у Date.FormatStyle, у ISO8601FormatStyle есть статическая переменная iso8601, которую мы задействуем. Эта функция форматирования очень похожа на ту, что была в предыдущем примере, только здесь доступны дополнительные настройки:

let date = Date.now

let stringDate = 
   date.formatted(
      .iso8601
      .month(.twoDigits)
      .day(.twoDigits)
      .year()
      .dateSeparator(.dash)
   )

print(stringDate) // 2021-07-18

С помощью ISO8601FormatStyle указывается разделитель компонентов даты, который будет использоваться.

Наконец, когда нас интересует только отображение строки даты, но конкретных требований к форматированию не имеется, задействуем последний вариант (номер четыре) и предопределенные форматы. Date.FormatStyle.DateStyle и Date.FormatStyle.TimeStyle предоставляют нам несколько готовых к использованию статических констант:

let date = Date.now

let stringDate = 
   date.fromatted(date: .long, time: .omitted)

print(stringDate) // July 18, 2021

Последняя, недостающая часть  —  обратная. Как создается тип даты Date из строки String с конкретным форматом? Для этого необходимо использовать инициализатор новой даты:

init<T, Value>(_ value: Value, strategy: T) throws 
  where T : ParseStrategy, Value : StringProtocol,
        T.ParseInput == String, T.ParseOutput == Date

Нужно передать функции строку String (которая будет датой Date строкового типа с пользовательским форматом), а также стратегию для выполнения парсинга этой строки. Параметр этой стратегии должен быть типа ParseStrategy. Как и FormatStyle, ParseStrategy является протоколом, имеющим два связанных типа, input и output. Input должен быть строкой String, а output  —  датой Date.

Но здесь так же, как в FormatStyle.Date, у нас уже есть встроенная структура Date.ParseStrategy, соответствующая протоколу ParseStrategy. Для ее использования в качестве параметра внутри функции init даты Date нужно только создать новый экземпляр:

init(
   format: Date.FormatString, 
   locale: Locale? = nil, 
   timeZone: TimeZone, 
   calendar: Calendar = Calendar(identifier: .gregorian), 
   isLenient: Bool = true, 
   twoDigitStartDate: Date = Date(timeIntervalSince1970: 0)
)

Представьте теперь, что мы ожидаем получить строку даты из бэкенда в формате день–месяц–год (например, 31–01–2021). Давайте сразу создадим сначала экземпляр ParseStrategy, а затем  —  новый экземпляр даты Date с помощью парсинга:

let parseStrategy = 
   Date.ParseStrategy(
      format: "\(day: .twoDigits)-\(month: .twoDigits)-\(year: .defaultDigits)",
      locale: Locale(identifier: "es"),
      timeZone: .current
   )

let serverDate = try? Date("01-08-2021", strategy: parseStrategy)

Так как здесь формат типа Date.FormatString, то для создания формата даты используем инициализатор интерполяции в сочетании с Date.FormatStyle.Symbol.

Имейте в виду, что на момент написания этой статьи весь изложенный в ней функционал находился на стадии бета-тестирования и в официальном выпуске возможны изменения.

Предлагаем также вашему вниманию шпаргалку с большинством вариантов, создаваемых с помощью нового API.

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

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Bruno Lorenzo: New Date Formatter API in Swift

Предыдущая статьяОсновы безопасного программирования
Следующая статьяВведение в потоки Redis