New Formatters in iOS 15: Why do we need another formatter

⋅ 10 min read ⋅ Foundation Swift iOS 15

Table of Contents

In iOS 15, we got a new Formatter API. Apple provides a new formatter across the board, numbers, dates, times, and more. Why do we need another formatter? How does it differ from the old one? Let's find out.

What does new formatter can do

The new formatters provide a simple interface for present data in a localized format string.

You can easily support sarunw.com by checking out this sponsor.

Sponsor sarunw.com and reach thousands of iOS developers.

What about old API

Before digging into the new formatter API, I want to assure you that the old API doesn't go anywhere. The new formatter API is all about taking data, like numbers, dates, times, and more and converting it into a localized user-presentable string. What the new formatter can do is a subset of what the old formatter can.

Most formatters we have right now are not just simple formatter but converters. They can convert data to string and vice-versa. For example, DateFormatter can convert string to date and date to string.

String to Data

Let's use DateFormatter as an example. We will try to convert the following string into a date.

28-06-2021

To convert this string, we create DateFormatter with the following properties.

let formatter = DateFormatter()
formatter.dateFormat = "dd-MM-yyyy" // 1
formatter.timeZone = TimeZone(secondsFromGMT: 60 * 60 * 7) // 2
formatter.calendar = Calendar(identifier: .gregorian) // 3

print(formatter.date(from: "28-06-2021"))
// 2021-06-27 17:00:00 +0000

<1> We specified a format of passing string.
<2> A time zone, in this case, is GMT+7 (Bangkok timezone)
<3> We use the gregorian calendar.

As you can see, there are some configurations (time zone and calendar) that we need to know before we can convert any string to a date. If that information isn't in the string, we have to put it in the formatter class to be able to do the conversion. Formatter will use device locale and time zone, which may vary based on devices.

This kind of conversion is complicated since it includes a lot of moving parts. It can't be any easier, and this string to data conversion is not what the new API wants to improve.

Data to String

Converting data to a string is another story. It can be complicated or easy based on the purpose of the result string. Let's see two examples of data conversion.

Convert to an arbitrary string.
Convert to localized user-facing string.

Convert to an arbitrary string

You do this kind of conversion when you have a specific string format in mind. One example is when you try to send date data back to your backend.

In this example, we convert a date to dd/MM/yyyy format.

let date = Date()
// 2021-06-28 16:17:44 +0000
let formatter = DateFormatter()
formatter.dateFormat = "dd/MM/yyyy"
formatter.calendar = Calendar(identifier: .gregorian)
print(formatter.string(from: date))
// 28/06/2021

formatter.timeZone = TimeZone(secondsFromGMT: 60 * 60 * 8) // 1
print(formatter.string(from: date))
// 29/06/2021

<1> Setting a different timezone may result in a wrong date, 28/06/2021 and 29/06/2021.

This case is as complicated as convert string to data. You must be as explicit as you can to get the result you want. In the above example, a different time zone leads to a different date string, so you need to be explicit about this.

Convert to localized user-facing string

We convert data to a string intended for users to read. This is the case that the new API wants to improve. Let's find out the reason for the improvement.

In this example, we format the current date into user-facing format.

let date = Date()
// 2021-06-28 16:33:02 +0000
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .none

print(formatter.string(from: Date())
// 6/25/21

We don't need to specify the locale, time zone, and calendar in this case. It will use the default value set in the device and the user's setting, which is the behavior we want. Some users might interpret 6/25/21 in a dd/MM/yy format, while some might interpret it as MM/dd/yy or else. This is totally based on their preference, and we shouldn't try to assume or hard code these values.

As you can see, this kind of conversion doesn't need a lot of configuration or explicit setting. All you need is to specify what field (day, month, year) you want to show and leave the implementation detail to the system, e.g., order and format.

Format user-facing string is a simpler operation, but it was build right into the more complicated API. The formatter means to do much more than that. This is the case where the new formatter API is trying to improve.

The problems of the old approach

Why do we need a new API with an overlap function with the old one?

As you can see from What about old API, there is nothing wrong with the old approach. Apple just sees an opportunity to improve usability for one specific case, convert data into a user-facing string. I think it easier to think of the old API as a converter and this new API as a real data formatter.

New formatter API build from the ground up to tackle this usability issue. To understand the need for a new API, we need to take a closer look at how we format data to user-facing strings in the old API.

Complexity

Each formatter has its complexity and convention. Let's use the DateFormatter as an example.

Here is an example of using DateFormatter to format a date.

class MessageTableViewCell: UITableViewCell {
private static let dateFormatter: DateFormatter = { // 1
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .none

return formatter
}()

func configure(with message: Message) {
...
dateLabel?.text = MessageTableViewCell.dateFormatter.string(from: message.createdDate)
...
}
}

Date formatter is backed by many configurations which expensive to create, so it's a common pattern to cache and reuse them <1>. But nothing enforces us to do so, and it is not very obvious that we need to do it. It is not mentioned anywhere in the documentation. I know this because it was mentioned in one of the WWDC sessions (which I also can't remember).

Also, having to create a formatter for each data type and cache them is not convenient.

Error-prone

Some formatter is easy to use, and some can be tricky. For example, you need to specify your own format via the dateFormat property if you want a custom date formatting.

Here is an example of a small error that cause a big different in output.

let formatter = DateFormatter()
formatter.dateFormat = "DD MMM yyyy"
// 175 Jun 2021

formatter.dateFormat = "dd MMM yyyy"
// 24 Jun 2021

In the above example, using DD instead of dd results in a totally different meaning. dd represents the day of the month while DD represents the day of the year.

Let's see another example from a number formatter.

In this example, we want to print a floating-point number with a precision of one.

print(String(format: "%.1f", 2)) // 1
// 0.0

print(String(format: "%.1f", 2.0))
// 2.0

<1> Using an integer result in a wrong output.

Put a non-floating point number, and the result is entirely wrong.

New approach

Convert data to a localized string is simpler than the conversion. Having to use the same interface for formatting and conversion makes the formatting job complicated than it should.

Apple solves these usability issues in twofold solution.

Remove formatter creation

The new API makes it simpler by removing the formatter creation process. You don't have to worry whether you have to cache it or not. You don't have to pass it around or set any values. With a new API, you don't have to bother creating it.

New formatted method

Apple introduces a new instance method for all data types that support formatting, .formatted.

Since we no longer use formatter objects, we can't pass data through the formatter anymore. The new API is in the form of extension over data.

Here is an example of a new date formatter API.

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

Here is a comparison of the old API and the new API.

// Old API
class MessageTableViewCell: UITableViewCell {
private static let dateFormatter: DateFormatter = { // 1
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .none

return formatter
}()

func configure(with message: Message) {
...
dateLabel?.text = MessageTableViewCell.dateFormatter.string(from: message.createdDate)
...
}
}

// New API
class MessageTableViewCell: UITableViewCell {
@IBOutlet var dateLabel: UILabel!

func configure(with message: Message) {
...
dateLabel?.text = message.createdDate.formatted(date: .numeric, time: .omitted) // 2
...
}
}

<1> New API no longer need to create a formatter.
<2> We call .formatted directly on the date. .short date style becomes .numeric and .none become .omitted in new API.

The new API result in a better separation of concern. We leave the old formatter as is. It can continue doing conversion as it always does. For user-facing formatting, we use the new .formatted method.

Benefits

This new API improves usability in many areas as follow:

Uniformity

We can now format our data in one unified way by calling .formatted on any data type. Here are some example formatters.

Date

Date().formatted()
// 6/28/2021, 1:38 PM

Date().formatted(date: .long, time: .omitted)
// June 28, 2021

Date().formatted(.dateTime.year())
// Jun 2021

Number

0.2.formatted()
// 0.2

0.2.formatted(.number.precision(.significantDigits(2)))
// 0.20

1.5.formatted(.currency(code: "thb"))
// THB 1.50

List

["Alice", "Bob", "Trudy"].formatted()
// Alice, Bob, and Trudy

["Alice", "Bob", "Trudy"].formatted(.list(type: .or))
// Alice, Bob, or Trudy

You can just use formatted without argument, and you will get the most sensible string representation over that data type or modify it with extra arguments to fit your need.

Compile-time checking

You don't have to do guesswork or looking back and forth between documentation. Everything is type-safe, so you get the benefit of compile-time checking.

You don't have to remember whether it is MMM or MMMM. You can explicitly specify the month format, in this case, .abbreviated and .wide.

Date().formatted(.dateTime.year().month(.abbreviated).day())
// Jun 2021 (MMM yyyy)

Date().formatted(.dateTime.year().month(.wide).day())
// June 2021 (MMMM yyyy)

Declarative

You declare what fields you want to show, and the system will do the hard work, presenting it in a suitable format. That's means you don't need to care about the order, locale, preference, or anything.

Date().formatted(.dateTime.year().month().day()) // 1
// Jun 28, 2021

Date().formatted(.dateTime.day().month().year()) // 2
// Jun 28, 2021

Date().formatted(.dateTime.day().month(.twoDigits).year()) // 3
// 06/28/2021

<1>, <2> .dateTime.year().month().day() and .dateTime.day().month().year() yield the same result even different order.
<3> Specified .twoDigits in .month make day and year present in the same format.

You can easily support sarunw.com by checking out this sponsor.

Sponsor sarunw.com and reach thousands of iOS developers.

Conclusion

This new formatter API doesn't replace the old one. You still need old formatters to do the conversion. I see it as a more focused version of the formatter that converts data to a localized user-facing string.

To do that, Apple remove the complexity of the original API and make it easier to use. We can get a sensible representation of our data with one line of code. Although the interface is simple, we still can format it like we used to, but it is better since we compile-time checking this time. You can't be wrong with this new API.

I think it is an API we don't know we always needed.


Read more article about Foundation, Swift, iOS 15, or see all available topic

Enjoy the read?

If you enjoy this article, you can subscribe to the weekly newsletter.
Every Friday, you'll get a quick recap of all articles and tips posted on this site. No strings attached. Unsubscribe anytime.

Feel free to follow me on Twitter and ask your questions related to this post. Thanks for reading and see you next time.

If you enjoy my writing, please check out my Patreon https://www.patreon.com/sarunw and become my supporter. Sharing the article is also greatly appreciated.

Become a patron Buy me a coffee Tweet Share
Previous
How to share an iOS distribution certificate

Learn how to create, export, and import certificate without any third-party tools.

Next
How to manually add existing certificates to the Fastlane match

Learn how to import .cer and p12 to Fastlane match without nuke or creating a new one.

← Home