New Formatters in iOS 15: Why do we need another formatter
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.
AI Grammar: Correct grammar, spell check, check punctuation, and parphrase.
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.
AI Grammar: Correct grammar, spell check, check punctuation, and parphrase.
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 ShareHow to share an iOS distribution certificate
Learn how to create, export, and import certificate without any third-party tools.
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.