Swift Foundation Formatter Improvements

Apple added a much easier way to work with date, number and other data formatters in iOS 15 and macOS 12.

Performance and Usability

The Foundation framework has a variety of formatters for working with dates, numbers, lists, measurements, person names and byte counts. They all have the aim of producing a correctly formatted string, localized for display to a user.

If you read much about using any of these formatters, you’ll find people warning you that creating them is expensive. This leads you to caching and reusing formatters in your apps.

In iOS 15 (and macOS 12) Apple wanted to improve this situation. To quote from the WWDC21 session on Foundation:

This year, we improved both performance and usability by rethinking our Formatter API from the ground up. In short, our new APIs focus on the format.

Unfortunately the documentation is not great so here’s a summary starting with date formatting.

Date Formatting

Before iOS 15, a typical use of a date formatter requires you to create the formatter, configure it and then use it to produce a formatted, localized string from a Date:

let now = Date()            // Jan 23, 2022 at 2:15 PM
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .long
formatter.string(from: now) // Jan 23, 2022 at 2:15:36 PM GMT

Starting with iOS 15 you apply the formatting directly to the Date without the need to create (and cache) a formatter. For example, the formatted(date:time) method applies predefined date and time styles:

now.formatted(date: .abbreviated, time: .standard)
// Jan 23, 2022, 2:15:36 PM

You can use a style of .omitted for either the date or time if you don’t want them to appear in the result. There are four date styles:

now.formatted(date: .abbreviated, time: .omitted) // Jan 23, 2022
now.formatted(date: .complete, time: .omitted)    // Sunday, January 23, 2022
now.formatted(date: .long, time: .omitted)        // January 23, 2022
now.formatted(date: .numeric, time: .omitted)     // 1/23/2022

The three time styles:

now.formatted(date: .omitted, time: .complete)    // 2:15:36 PM GMT
now.formatted(date: .omitted, time: .shortened)   // 2:15 PM
now.formatted(date: .omitted, time: .standard)    // 2:15:36 PM

For greater flexibility you can start from one of two possible format styles:

now.formatted(.dateTime) // 1/23/2022, 2:15 PM
now.formatted(.iso8601)  // 2022-01-23T14:15:36Z
now.formatted()          // same as .dateTime

You customize the output by adding fields to the style. The output then contains just the fields you want:

now.formatted(.dateTime.year().day().month()) // Jan 23, 2022

The order you add the fields doesn’t matter. The formatter produces a localized order based on the fields you include:

now.formatted(.dateTime.hour().minute().month().day())
// Jan 23, 2:15 PM

You can override the locale:

let en_GB = Locale(identifier: "en_GB")
now.formatted(.dateTime.year().day().month().locale(en_GB)) 
// 23 Jan 2022

Some of the fields have further options. Some examples:

now.formatted(.dateTime.day())                     // 23
now.formatted(.dateTime.day(.ordinalOfDayInMonth)) // 4 (4th week)
now.formatted(.dateTime.dayOfYear(.threeDigits))   // 023
now.formatted(.dateTime.era(.wide))                // Anno Domini
now.formatted(.dateTime.hour(.twoDigits(amPM: .narrow))) // 02 p 
now.formatted(.dateTime.quarter())                 // Q1
now.formatted(.dateTime.timeZone(.exemplarLocation)) // London
now.formatted(.dateTime.week(.weekOfMonth))        // 5
now.formatted(.dateTime.weekday(.short))           // Su

The Xcode documentation is often missing but I find that typing . is enough to trigger the Xcode autocomplete to show you what’s possible with examples:

Xcode autocomplete showing example of .day usage

The .iso8601 format has some different options:

now.formatted(.iso8601) // 2022-01-23T14:15:36Z
now.formatted(.iso8601.time(includingFractionalSeconds: true))
// 14:15:36.000
now.formatted(.iso8601.dateTimeSeparator(.space))
// 2022-01-23 14:15:36Z

Attributed Strings

If you add the .attributed field to a format you get back a formatted attribute string. This is handy when you want to format components of the output:

var attributed = now.formatted(.dateTime.attributed)

Create an attribute container for the date field you want to format and another for the attribute you want to apply. For example, to change the SwiftUI foreground color of the timezone:

struct DateView: View {
  @State var now = Date.now

  var coloredDate: AttributedString {
    var attributed = now.formatted(.dateTime
                                   .hour()
                                   .minute(.twoDigits)
                                   .second(.twoDigits)
                                   .timeZone()
                                   .attributed)
      
    let timezone = AttributeContainer.dateField(.timeZone)
    let color = AttributeContainer.foregroundColor(.red)
    attributed.replaceAttributes(timezone, with: color)
    return attributed
  }
    
  var body: some View {
    Text(coloredDate)
      .font(.title)
  }
}

14:24:59 GMT with the GMT timezone in red

Check the AttributedString docs for AttributeScopes to see the values allowed for Foundation, SwiftUI, UIKit and AppKit. Make sure you have the headers imported so Xcode autocomplete works.

Other Foundation Formatters

The formatted API works for the other formatters. Here’s a selection of examples to get you started:

DateInterval

(now..<later).formatted(.timeDuration) // 5:00
(now..<later).formatted(.interval)     // 1/23/22, 2:15-2:20 PM

RelativeDate

fiveLater.formatted(.relative(presentation: .numeric))
// in 5 minutes
yesterday.formatted(.relative(presentation: .named,
  unitsStyle: .spellOut))
// two days ago

Number

let count = 1_200_450
count.formatted() // 1,200,450.12
count.formatted(.number.notation(.scientific)) // 1.20045E6

let percent: Double = 18/48
percent.formatted(.percent) // 37.5%

let profit = 1450.28
profit.formatted(.currency(code: "eur"))

List

let colors = ["red", "green", "blue"]
colors.formatted()                 // red, green and blue
colors.formatted(.list(type: .or)) // red, green or blue

Measurement

let length = Measurement(value: 1230, unit: UnitLength.meters)
length.formatted()                             // 4,035 ft
length.formatted(.measurement(width: .narrow)) // 4,035'

PersonNameComponents

let person = PersonNameComponents(namePrefix: "Sir",
  givenName: "Arthur", middleName: "Conan",
  familyName: "Doyle")
person.formatted()                    // Arthur Doyle
person.formatted(.name(style: .long)) // Sir Arthur Conan Doyle

ByteCount

let size = 1_845_200_876
size.formatted(.byteCount(style: .memory))  // 1.72 GB
size.formatted(.byteCount(style: .decimal)) // 1.85 GB
size.formatted(.byteCount(style: .memory,
  allowedUnits: .all, spellsOutZero: true,
  includesActualByteCount: true))
// 1.72 GB (1,845,200,876 bytes)

Further Details