В версии 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.
Читайте также:
- Построение бесконечного списка с помощью SwiftUI и Combine
- Диспетчер загрузки на Swift
- Двоичный поиск в Swift и расширение возможностей коллекций
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Bruno Lorenzo: New Date Formatter API in Swift