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
.
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 withAttributeContainer
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!