Modernizing date formatting in Swift with a new approach
Serhii Petrishenko
Advanced Software Engineer (iOS)
May 31, 2023 8 min read
Serhii Petrishenko
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
Copied
show more show less

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
Copied
show more show less

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
Copied
show more show less

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
Copied
show more show less

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
Copied
show more show less

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
Copied
show more show less

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
Copied
show more show less

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
Copied
show more show less
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
Copied
show more show less

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
Copied
show more show less

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
Copied
show more show less

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
Copied
show more show less
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()
Copied
show more show less

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)
Copied
show more show less

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

try? formatStyle.parseStrategy.parse(dateStr)
Copied
show more show less

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)
Copied
show more show less

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)
Copied
show more show less
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)
  }
 
}
Copied
show more show less

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 р.
Copied
show more show less
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

Get in touch

    Thank you for your interest in Innovecs. If you're a client or a job seeker, the perfect way to connect with us awaits you below.
    Your CV has landed in our inbox, and we couldn't be happier! If your skills and experiences match the position requirements, we will be sure to get in touch with you.

    We appreciate your patience and your interest in being a part of our team.