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:
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)
}
}
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)