Creating Custom Parseable Format Styles in iOS 15

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()
    }
}
Using a SwiftUI TextField with Custom Formatting

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.

Published by Mark Thormann

As a software developer and architect, I enjoy using technology to craft solutions to business problems, focusing on all aspects of native iOS and Android mobile development as well as application architecture, automation. and many other areas of expertise. I'm currently working at one of the leading career-related companies in the United States, using mobile applications to help connect job seekers in the technology industry to the employment which they need.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: