Since way back in iOS 2.0, the venerable Formatter
class and its derivations such as DateFormatter
and NumberFormatter
have been the go-to for converting values to and from strings. Over the years Apple has added other cool formatters such as the ByteCountFormatter
, MeasurementFormatter
, RelativeDateTimeFormatter, and ListFormatter
. Now, as discussed in What’s New in Foundation at WWDC21, we have a whole new way to convert values!
In this post, we’ll look at these new capabilities and dig deep to see how to extend the new ParseableFormatStyle
and related protocols to make our own custom phone number formatter. Then we’ll use that to actually format SwiftUI TextField input. Please note that while I will be focusing on iOS here, these new protocols are available in macOS, watchOS, and tvOS as well as of Xcode 13.
The Old Formatter Way
Prior to iOS 15 and Xcode 13, in order to convert a number or date to a string, you would have had to do something like the following:
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .long
dateFormatter.timeStyle = .none
let formattedDate = dateFormatter.string(from: Date())
// August 14, 2021
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .currency
numberFormatter.maximumFractionDigits = 0
numberFormatter.currencyCode = "USD"
let formattedNumber = numberFormatter.string(from: 1234)
// $1,234
First you’d instantiate an appropriate Formatter
class object, then adjust properties to control the input or output formats, and finally call a method to return the desired string. While this works (and will continue to), there are a couple downsides to this approach:
Formatter
derived classes can be expensive to instantiate and maintain and, if not used correctly in looping, repetitive, or other situations, can lead to slowness and excess resource usage.- Coding formatters often requires specific syntax (date format strings, anyone?), usually takes multiple lines of code, and setting multiple properties on the
Formatter
object. This make the coding process more complicated/slow and less maintainable.
A Simpler Way to Create Formatted Strings
With iOS 15 (and the latest versions for watch, iPad, etc.), we can now convert certain values such as dates and numbers to strings much more easily while still maintaining specific control over the process. For example, we can now create strings like so using nice, 1-line, fluent syntax.
let formattedDate = Date().formatted(.dateTime.month(.wide).year().day())
// August 14, 2021
let formattedNumber = 1234.formatted(.currency(code: "USD").precision(.fractionLength(0)))
// $1,234
Simpler! As you can see, no Formatter
variant is needed and we can do most of what we need to do in just one line of code. Code completion and the online documentation can guide you through the basics of these formatters; but we want to take things a step further.
Creating Your Own Custom Formatter
So let’s assume that we have some other type of data, such as a custom object that may contain a phone number, that we want to parse and output in a very specific but flexible way. Previously we would have had to sub-class the Formatter
object, instantiate the new class, and format like we did above in our initial example. Now with several new protocols, including ParseableFormatStyle
and ParseStrategy
, we can write our own code that looks and behaves like Apple’s built in formatting and parsing code for dates and numbers. So let’s do it!
The Key New Formatting Protocols
At the core of the new functionality are a couple important new protocols. The first is ParseableFormatStyle
which exposes, through the FormatStyle
protocol, formatting and locale-specific functions. Objects that can manipulate data from one type to another can call the format(_:)
method, shown below, to do so.
/// A type that can convert a given data type into a representation.
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
public protocol ParseableFormatStyle : FormatStyle {
associatedtype Strategy : ParseStrategy where Self.FormatInput == Self.Strategy.ParseOutput, Self.FormatOutput == Self.Strategy.ParseInput
/// A `ParseStrategy` that can be used to parse this `FormatStyle`'s output
var parseStrategy: Self.Strategy { get }
}
/// A type that can convert a given data type into a representation.
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
public protocol FormatStyle : Decodable, Encodable, Hashable {
/// The type of data to format.
associatedtype FormatInput
/// The type of the formatted data.
associatedtype FormatOutput
/// Creates a `FormatOutput` instance from `value`.
func format(_ value: Self.FormatInput) -> Self.FormatOutput
/// If the format allows selecting a locale, returns a copy of this format with the new locale set. Default implementation returns an unmodified self.
func locale(_ locale: Locale) -> Self
}
When we create our own parser for our object type, we’ll also need to implement the new ParseStrategy
protocol to control the actual parsing process from the formatted type back to the type being formatted.
/// A type that can parse a representation of a given data type.
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
public protocol ParseStrategy : Decodable, Encodable, Hashable {
/// The type of the representation describing the data.
associatedtype ParseInput
/// The type of the data type.
associatedtype ParseOutput
/// Creates an instance of the `ParseOutput` type from `value`.
func parse(_ value: Self.ParseInput) throws -> Self.ParseOutput
}
Creating our Parsing Strategy
So let’s start our new custom phone number format style! For our example, we’re going to create a simple PhoneNumber
object that we need to convert into a standard U.S. formatted phone number string with the style (<area code>) <exchange>-<number>. As a bonus, we’d also like to be able to remove the parentheses, hyphen, and/or space between the area code and number as well as maybe display the number without the area code.
/// Representation of U.S. phone number
public struct PhoneNumber {
/// Area code
public var areaCode: String
/// First three digits of a 7-digit phone number
public var exchange: String
/// Last four digits of a 7-digit phone number
public var number: String
}
Now that our object type has been defined, our next step is going to be to create a ParseStrategy
to allow conversion from a String
type back to our custom PhoneNumber
type. There are obviously many ways to implement the actual parsing but this will do for our fairly simplistic example here.
public struct PhoneNumberParseStrategy: ParseStrategy {
/// Creates an instance of the `ParseOutput` type from `value`.
/// - Parameter value: Value to convert to `PhoneNumber` object
/// - Returns: `PhoneNumber` object
public func parse(_ value: String) throws -> PhoneNumber {
// Strip out to just numerics. Throw out parentheses, etc. Simple version here ignores country codes, localized phone numbers, etc. and then convert to an array of characters
let maxPhoneNumberLength = 10
let numericValue = Array(value.filter({ $0.isWholeNumber }).prefix(maxPhoneNumberLength))
// PUll out the phone number components
var areaCode: String = ""
var exchange: String = ""
var number: String = ""
for i in 0..<numericValue.count {
switch i {
case 0...2:
// Area code
areaCode.append(numericValue[i])
case 3...5:
// Exchange
exchange.append(numericValue[i])
default:
// Number
number.append(numericValue[i])
}
}
// Output the populated object
return PhoneNumber(areaCode: areaCode, exchange: exchange, number: number)
}
}
In the example above we’re just taking our input string, removing any non-numerics and only allowing ten characters before breaking apart the components. Please note once again this is a simple, non-localized example used to illustrate these concepts.
Adding the Custom Parseable Format Style
Now that our strategy has been defined, we can create a struct
with our new PhoneNumberFormatStyle
object.
public extension PhoneNumber {
/// Phone number formatting style
struct PhoneNumberFormatStyle {
/// Pieces of the phone number
enum PhoneNumberFormatStyleType: CaseIterable, Codable {
case parentheses // Include the parentheses around the area code
case hyphen // Include the hyphen in the middle of the phone number
case space // Include the space between area code and phone number
case areaCode // Area code
case phoneNumber // Phone number itself
}
/// Type of formatting
var formatStyleTypes: [PhoneNumberFormatStyleType] = []
/// Placeholder character
var placeholder: Character = "_"
/// Constructor w/placeholder optional
/// - Parameter placeholder: Placeholder to use instead of '_'
init(placeholder: Character = "_") {
self.placeholder = placeholder
}
/// Constructer to allow extensions to set formatting
/// - Parameter formatStyleTypes: Format style types
init(_ formatStyleTypes: [PhoneNumberFormatStyleType]) {
self.formatStyleTypes = formatStyleTypes
}
}
}
There’s a little bit going on here but basically we’ve created an extension on our custom PhoneNumber
struct with an enum, constructors, and properties to allow customization of the formatted output.
After creating the base object, we now need to actually implement ParseableFormatStyle
and define our formatting. In the code below you can also see us exposing the ParseStrategy
that we defined above (for going from String
to PhoneNumber
) and the format(_:)
function where we output a custom string based on the enum
and placeholder
settings.
extension PhoneNumber.PhoneNumberFormatStyle: ParseableFormatStyle {
/// A `ParseStrategy` that can be used to parse this `FormatStyle`'s output
public var parseStrategy: PhoneNumberParseStrategy {
return PhoneNumberParseStrategy()
}
public func format(_ value: PhoneNumber) -> String {
// Fill out fields with placeholder
let stringPlaceholder = String(placeholder)
let paddedAreaCode = value.areaCode.padding(toLength: 3, withPad: stringPlaceholder, startingAt: 0)
let paddedExchange = value.exchange.padding(toLength: 3, withPad: stringPlaceholder, startingAt: 0)
let paddedNumber = value.number.padding(toLength: 4, withPad: stringPlaceholder, startingAt: 0)
// Get the working style types
let workingStyleTypes = !formatStyleTypes.isEmpty ? formatStyleTypes : PhoneNumberFormatStyleType.allCases
var output = ""
if workingStyleTypes.contains(.areaCode) {
output += workingStyleTypes.contains(.parentheses) ? "(" + paddedAreaCode + ")" : paddedAreaCode
}
if workingStyleTypes.contains(.space) && workingStyleTypes.contains(.areaCode) && workingStyleTypes.contains(.phoneNumber) {
// Without the area code and phone number, no point with space
output += " "
}
if workingStyleTypes.contains(.phoneNumber) {
output += workingStyleTypes.contains(.hyphen) ? paddedExchange + "-" + paddedNumber : paddedExchange + paddedNumber
}
// All done
return output
}
}
After doing this we also need to implement Codable (since FormatStyle
implements Codable
and Hashable
) to persist the state of the format style. You can download the source code for this article to see this and more.
We also want to expose methods to allow us to construct a specific format fluently just like the styles built into the latest iOS system objects. Below, the area code, phone number, and punctuation-related methods are defined to do just that.
/// Publicly available format styles to allow fluent build of the style
public extension PhoneNumber.PhoneNumberFormatStyle {
/// Return just the area code (e.g. 617)
/// - Returns: Format style
func areaCode() -> PhoneNumber.PhoneNumberFormatStyle {
return getNewFormatStyle(for: .areaCode)
}
/// Return just the phone number (e.g. 555-1212)
/// - Returns: Format style
func phoneNumber() -> PhoneNumber.PhoneNumberFormatStyle {
return getNewFormatStyle(for: .phoneNumber)
}
/// Return the space between the area code and phone number
/// - Returns: Format style
func space() -> PhoneNumber.PhoneNumberFormatStyle {
return getNewFormatStyle(for: .space)
}
/// Return the parentheses around the area code
/// - Returns: Format style
func parentheses() -> PhoneNumber.PhoneNumberFormatStyle {
return getNewFormatStyle(for: .parentheses)
}
/// Return the hyphen in the middle of the phone number
/// - Returns: Format style
func hyphen() -> PhoneNumber.PhoneNumberFormatStyle {
return getNewFormatStyle(for: .hyphen)
}
/// Get a new phone number format style
/// - Parameter newType: New type
/// - Returns: Format style
private func getNewFormatStyle(for newType: PhoneNumberFormatStyleType) -> PhoneNumber.PhoneNumberFormatStyle {
if !formatStyleTypes.contains(newType) {
var newTypes = formatStyleTypes
newTypes.append(newType)
return PhoneNumber.PhoneNumberFormatStyle(newTypes)
}
// If the user duplicated the type, just return that type
return self
}
}
We’re done, right?! Well not quite. Just a couple little pieces are left to leverage the .formatted(_:)
syntax, used by the pre-defined format styles for Date
and Number
for example.
public extension PhoneNumber {
func formatted(_ formatStyle: PhoneNumberFormatStyle) -> String {
formatStyle.format(self)
}
}
public extension FormatStyle where Self == PhoneNumber.PhoneNumberFormatStyle {
/// Format the given string as a phone number in the format (___) ___-____ or similar
static var phoneNumber: PhoneNumber.PhoneNumberFormatStyle {
PhoneNumber.PhoneNumberFormatStyle()
}
}
Putting It All Together
Whew! That was a lot but hopefully you hung in there. So now let’s use the formatter we created for our PhoneNumber
object.
let phoneNumber = PhoneNumber(areaCode: "123", exchange: "555", number: "1212")
// Default
print(phoneNumber.formatted(.phoneNumber)) // (123) 555-1212
// No punctuation
print(phoneNumber.formatted(.phoneNumber.areaCode().number())) // 1235551212
// Just the last 7 digits and the hyphen
print(phoneNumber.formatted(.phoneNumber.number().hyphen())) // 555-1212
What’s really nice as well is that we can now use this custom format style in SwiftUI also to customize TextField
output with the new constructors based on ParseableFormatStyle
!
struct PhoneNumberTextField: View {
@Binding var phoneNumber: PhoneNumber
var body: some View {
TextField("Phone Number", value: $phoneNumber, format: .phoneNumber, prompt: Text("Enter phone number"))
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
}
}

There are some rough edges that we might want to clean up in this implementation but you can see how these new formatters can be leveraged in the UI directly as well as behind the scenes.
Get the Source Code and Explore
We’ve just scratched the surface here of what can be accomplished with the new formatting API’s. As you may have noticed, the default output type doesn’t have to be String
and can be any type that makes sense to your use case. By exposing ParseableFormatStyle
, ParseStrategy
, and other protocols, you can leverage custom format styles to improve your code.
If you want to dig in and try this out for yourself, please feel free to download the source code and play around with a sample project and unit tests. As always please let me know what you think in the comments and feel free to like/follow/share my blog.
This is a really useful blog post but it’s very hard to read the code since there’s no syntax highlight.
LikeLike
Thanks for the feedback and glad you like the post! I’ll try to improve that on the site going forward.
LikeLike