paint-brush
A New Approach to Date Formatting In Swiftby@serhiipetrishenko
4,161 reads
4,161 reads

A New Approach to Date Formatting In Swift

by Serhii PetrishenkoMay 8th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

In Swift 5.5 and iOS 15 Apple has introduced new interfaces that allow developers to convert data types from Foundation into localized strings. The main aim is to provide easier way to create formatted display string with less of customizations. In this article, we are going to focus on how to convert Dates and Date ranges to formatted localized string for display. We will also create our own custom format styles for Date.
featured image - A New Approach to Date Formatting In Swift
Serhii Petrishenko HackerNoon profile picture

What’s New in Foundation

In Swift 5.5 and iOS 15, Apple has introduced new interfaces that allow developers to convert data types from Foundation into localized strings and vice versa — FormatStyle, ParseableFormatStyle, and ParseStrategy protocols.


The main aim is to provide an easier way to create formatted display strings with less customization instead of using old Formatter subclasses such as DateFormatter, DateComponentsFormatter, DateIntervalFormatter, NumberFormatter, MeasurementFormatter, ByteCountFormatter, and PersonNameComponentsFormatter.


This approach supports dates, date ranges, numerics, measurements, sequences, durations (from iOS 16), URLs (from iOS 16), byte counts, and a person’s name components.


In this article, we are going to focus on how to convert Dates and Date ranges to formatted localized strings for display, parse Date objects from string constant, and create our own custom format styles for Dates.

Single Date Formatting

As you know, if you want to display the date in your app using, for example, some formats — short, long, or custom ("yyyy-MM-dd"), you need to create DateFormatter’s instances and set “dateStyle” or “dateFormat” (for custom style) properties:


extension Date {
  
  func string(formatter: DateFormatter) -> String {
    return formatter.string(from: self)
  }
  
}
extension DateFormatter {
  
  static var shortDateFormatter: DateFormatter {
    let formatter = DateFormatter()
    formatter.dateStyle = .short
    return formatter
  }
  
  static var longDateFormatter: DateFormatter {
    let formatter = DateFormatter()
    formatter.dateStyle = .long
    return formatter
  }
  
  static var customDateWithDashFormatter: DateFormatter {
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy-MM-dd"
    return formatter
  }
  
}
let date = Date()
print(date.string(formatter: .shortDateFormatter)) // 3/12/23
print(date.string(formatter: .longDateFormatter)) // March 12, 2023
print(date.string(formatter: .customDateWithDashFormatter)) // 2023-03-12


At first glance, everything seems fine here. But what if we need to add one more custom style to the display Date — "yyyy/MM/dd”? The main problem is we have to extend our date formatters by adding new:

extension DateFormatter {
  
  static var customDateWithSlashFormatter: DateFormatter {
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy/MM/dd"
    return formatter
  }
  
}
print(Date().string(formatter: .customDateWithSlashFormatter)) // 2023/03/12


Let’s take a look at how Apple helps us resolve that issue. They have created a few methods that assist us in solving the problem described above.

public func formatted() -> String


print(Date().formatted()) // 3/12/2023, 2:05 PM


The basic method that converts Date into a localized string using the default transformation style.

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


print(Date().formatted(.dateTime.day(.twoDigits).month(.twoDigits).year(.twoDigits))) // 03/12/23


As we see here, we must pass a parameter that conforms to the FormatStyle protocol. Apple took care of that too: Swift offers us a built-in struct that conforms to the FormatStyle protocol, and we do not need to create our own.


Also, it has a static variable “static var dateTime: Date.FormatStyle”.


Besides, we have the possibility to create a new instance of Date.FormatStyle:

print(Date().formatted(Date.FormatStyle().day().month(.wide).year())) // March 12, 2023


Date.FormatStyle has a lot of methods like “day(…), month(…), year(…), etc.”; they have their own parameters for customization and return the same type (Date.FormatStyle type). These instance methods offer us many options for creating a multitude of options.


Furthermore, we do not need to worry about the order we call the functions; Swift takes care of that and chooses the correct format based on the user's preferences.

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


print(Date().formatted(date: .long, time: .omitted)) // March 12, 2023


Here, Swift provides some default pre-defined format styles for date and time — Date.FormatStyle.DateStyle and Date.FormatStyle.TimeStyle.

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


print(Date().ISO8601Format(.iso8601.day().month().year().dateSeparator(.dash))) // 2023-03-12


It is used to convert Date to localized strings using iso8601 format. Date.ISO8601FormatStyle also has a static variable “static var iso8601: Date.ISO8601FormatStyle” and has similar methods like Date.FormatStyle. Moreover, we can use “formatted(…)” method to do the same:

print(Date().formatted(.iso8601.day().month().year().dateSeparator(.dash))) // 2023-03-12

Date Range Formatting


For date ranges, likewise, it is easy to use the same approach to create a localized formatted string because Swift provides very similar methods that we have for Date:

public func formatted() -> String
public func formatted<S>(_ style: S) -> S.FormatOutput where S : FormatStyle, S.FormatInput == Range<Date>
public func formatted(date: Date.IntervalFormatStyle.DateStyle, time: Date.IntervalFormatStyle.TimeStyle) -> String


We see that we already have “static var interval: Date.IntervalFormatStyle” and can use it in the same way as we use Date.FormatStyle:

let dateRange = Date(timeInterval: -3600, since: Date())..<Date()
print(dateRange.formatted(.interval.day().month(.wide).year().minute().hour())) // March 12, 2023, 5:18 – 6:18 PM


Or create a new instance of Date.IntervalFormatStyle:

print(dateRange.formatted(Date.IntervalFormatStyle().day().month(.wide).year().minute().hour())) // March 12, 2023, 5:18 – 6:18 PM


Furthermore, you can find the time gap between the earliest and latest dates in a given date range using distinct units:

print(dateRange.formatted(.components(style: .wide, fields: [.hour]))) // 1 hour

Date Parsing

Swift provides several variants to transform a string into a Date object.


Let’s create a date string and a new instance of Date.FormatStyle with some customization that expects in what format our localized string constant can be parsed:

let dateStr = "March 12, 2023"
let formatStyle = Date.FormatStyle().day().month().year()


Apple introduced the ParseStrategy protocol to do such a task. ParseStrategy has two associated types: Input and Output, for Date input is String, and output is Date. By default, Date.FormatStyle conforms to that protocol, so we can use the method “parse(…)” directly:

try? formatStyle.parse(dateStr)


Or we can use ParseableFormatStyle’s property “parseStrategy” (Date.FormatStyle conforms to ParseableFormatStyle protocol by default too):

try? formatStyle.parseStrategy.parse(dateStr)


Swift provides a default parse strategy for Date — Date.ParseStrategy. It can be used with a new initializer for Date (for the format, we are using an interpolation initializer):

let parseStrategy = Date.ParseStrategy(format: "\(month: .wide) \(day: .defaultDigits), \(year: .defaultDigits)", locale: .current, timeZone: .current)
try? Date(dateStr, strategy: parseStrategy)


For Date.ISO8601FormatStyle, we can do the same manipulations:

let iso8601DateStr = "2023-03-12T18:06:55Z"
let iso8601FormatStyle = Date.ISO8601FormatStyle()
try? iso8601FormatStyle.parse(iso8601DateStr)
try? iso8601FormatStyle.parseStrategy.parse(iso8601DateStr)
try? Date(iso8601DateStr, strategy: iso8601FormatStyle)
try? Date(iso8601DateStr, strategy: iso8601FormatStyle.parseStrategy)

Creating Custom Format Styles

Suppose we need to convert some dates in our app into localized string constants using concrete Calendar or Locale. We should create our own FormatStyle’s object:

struct UkrainianLocaleFormatStyle: FormatStyle {
  
  typealias FormatInput = Date
  typealias FormatOutput = String
  
  private static let customFormatStyle = Date.FormatStyle(date: .long, time: .omitted, locale: Locale(identifier: "uk_UA"), calendar: Calendar(identifier: .gregorian))
  
  func format(_ value: Date) -> String {
    return Self.customFormatStyle.format(value)
  }
  
}


As we see, we have some requirements from FormatStyle protocol: for Input type, we have Date, for Output — String, for “format(…)” method, we have created a custom format style. For using our custom format style, we extend FormatStyle:

extension FormatStyle where Self == UkrainianLocaleFormatStyle {
  
  static var ukrainianLocale: UkrainianLocaleFormatStyle { return UkrainianLocaleFormatStyle() }
  
}
print(Date().formatted(.ukrainianLocale)) // 12 березня 2023 р.

Final Thoughts

In this article, we’ve made a brief overview of how to convert data types (in our example — Date) to and from localized strings.


Apple brought in a highly configurable instrument to format the built-in data types, allowing developers to customize the formatting rules to suit their specific needs.


They perform better and are simpler to use. However, there are some limitations:


  • FormatStyle is only available from iOS 15 and beyond, so if your projects have a lower version - you should use one of the old Formatter subclasses.


  • FormatStyle is not allowed in Objective-C.