Найти в Дзене
Nuances of programming

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

Источник: Nuances of Programming

В версии 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