Customize formatted currency amount in iOS 15

June 14, 2021

This year at WWDC 2021, Foundation framework got an interesting update with a Swifty AttributedString but also new Formatting API. You can watch the session here

What caught my attention, is the new AttributeScopes with FoundationAttributes that can be used to safely access a specific range of the AttributedString.

Map

For many years, in UIKit or even in SwiftUI I’ve had to customize how I display an amount inside a UILabel or a Text using CurrencyFormatter. But there isn’t much customization available, what if you want to emphasize more on the integer part rather than giving equal weight with the fraction part? For greater customization, start parsing string, use multiples UILabel/Text so that you can also play with the baseline and a few trial and error and you might end up with a working solution.

Well, starting from iOS 15 (also available in macOS, watchOS etc..) it looks like everything got simpler!

import SwiftUI

struct ContentView: View {
    @State private var amount: Double = Double.random(in: 10...99999)
    @State private var date: Date = .now

    var body: some View {
        Text(pretty(amount))
    }

    private func pretty(_ amount: Double) -> AttributedString {
        var str: AttributedString = amount.formatted(.currency(code: Locale.current.currencyCode!).attributed)

        // Integer
        let integer = AttributeContainer.numberPart(.integer)
        let integerAttributes = AttributeContainer
            .foregroundColor(.primary)
            .font(.system(.headline, design: .rounded))
        str.replaceAttributes(integer, with: integerAttributes)

        // Fraction
        let fraction = AttributeContainer.numberPart(.fraction)
        let fractionAttributes = AttributeContainer
            .foregroundColor(.secondary)
            .font(.system(.footnote, design: .rounded))
        str.replaceAttributes(fraction, with: fractionAttributes)

        // Currency symbol
        let symbol = AttributeContainer.numberSymbol(.currency)
        let symbolAttributes = AttributeContainer
            .foregroundColor(.purple)
            .font(.caption)
            .baselineOffset(3)
        str.replaceAttributes(symbol, with: symbolAttributes)

        // Decimal separator
        let decimal = AttributeContainer.numberSymbol(.decimalSeparator)
        let decimalAttributes = AttributeContainer
            .foregroundColor(.secondary.opacity(0.5))
        str.replaceAttributes(decimal, with: decimalAttributes)

        // Grouping separator
        let grouping = AttributeContainer.numberSymbol(.groupingSeparator)
        let groupingAttributes = AttributeContainer
            .font(.body)
            .foregroundColor(.secondary)
        str.replaceAttributes(grouping, with: groupingAttributes)

        return str
    }

That’s it. And that’s customizing way more than you should. One single SwiftUI.Text that can now render an AttributedString. All safely, using AttributeContainer to access .numberPart(.fraction) or .numberSymbol(.decimalSeparator).

To be honest, without detailed documentation it took me an hour to play around to find the correct API. So a tip if you are looking to format other elements in similar manner is to:

  • Use a Playground
  • Initialize your AttributedString by using the .formatted modifier along .attributed on your value (Date, String, Number…).
  • Access the .runs property which will give you some hints about what to look for with AttributeContainer subscript
// Example of `print(Double(1335.85).formatted(.currency(code: Locale.current.currencyCode!).attributed))`
$ {
	Foundation.NumberFormatSymbol = currency
}
1 {
	Foundation.NumberFormatPart = integer
}
, {
	Foundation.NumberFormatSymbol = groupingSeparator
	Foundation.NumberFormatPart = integer
}
335 {
	Foundation.NumberFormatPart = integer
}
. {
	Foundation.NumberFormatSymbol = decimalSeparator
}
85 {
	Foundation.NumberFormatPart = fraction
}

We can extend this similar approach from what was presented from Apple with Date to Currency but any other Formatters!


© Thomas Sivilay 2021, Built with Gatsby