Modernizing date formatting in Swift with a new approach

May 31, 2023 8 min read

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 easier way to create formatted display string with less of customizations 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 person’s name components.

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

Single Date Formatting

As you know, if you want to display Date into 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 display Date — “yyyy/MM/dd”? The main problem is we have to extend our date formatters with 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 problems described above.

 

public func formatted() -> String
 
print(Date().formatted()) // 3/12/2023, 2:05 PM

The basic method that converts Date into localized string using 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 a 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 about 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 string 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 like 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 to 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 ParseStrategy protocol to do such tasks. 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 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 default parse strategy for Date — Date.ParseStrategy. It can be used with new initializer for Date (for format we are using 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 constant using concrete Calendar or Locale. We should create our own FormatStyle 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 highly configurable instruments 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, so if your projects have a lower version – you should use one of the old Formatter subclasses.
  • FormatStyle is not allowed in Objective-C.

 

The article was originally published on HackerNoon

Serhii Petrishenko
Advanced Software Engineer (iOS)
Don't See Your Dream Job?