FormatStyle AttributedString Output

This is part of the FormatStyle Deep Dive series

Outputting AttributedString values instead of plain String values from a FormatStyle instance shows the hidden power of the FormatStyle protocol.

Apple has included support for AttributedString output in quite a few of their style implementations, and accessing it is as easy as calling .attributed.


Download the Xcode Playground with all examples

See the examples as a gist


While not all styles are supported by default, currently we have support for:

If you’re feeling limited by those provided (or if you have a fully custom FormatStyle), you can easily add support by rolling your own FormatStyle.

Details on outputting AttributedString values in custom FormatStyle implementations.


Examples

0.88.formatted(.percent.attributed)

Outputs:

Attributed String Output for 88%


You can then bring to bear the power of the new AttributedString API in order to modify every aspect of the text for display:

struct ContentView: View {
    var percentAttributed: AttributedString {
        var percentAttributedString = 0.8890.formatted(.percent.attributed)
        percentAttributedString.swiftUI.font = .title
        percentAttributedString.runs.forEach { run in
            if let numberRun = run.numberPart {
                switch numberRun {
                case .integer:
                    percentAttributedString[run.range].foregroundColor = .orange
                case .fraction:
                    percentAttributedString[run.range].foregroundColor = .blue
                }
            }

            if let symbolRun = run.numberSymbol {
                switch symbolRun {
                case .percent:
                    percentAttributedString[run.range].foregroundColor = .green
                case .decimalSeparator:
                    percentAttributedString[run.range].foregroundColor = .red
                default:
                    break
                }
            }
        }

        return percentAttributedString
    }

    var body: some View {
        VStack {
            Text(percentAttributed)
        }
        .padding()
    }
}

Will show:

Attributed String Output for 88%


Adding AttributedString output to Custom Format Styles

To have your custom FormatStyle output AttributedString values, you simply have to create another FormatStyle that simply sets it’s FormatOutput to AttributedString.

Once created, you can set the .attributed property on the original FormatStyle and call it:

struct ToYen: FormatStyle {
    typealias FormatInput = Int
    typealias FormatOutput = String

    static let multiplier = 100
    static let formatter = IntegerFormatStyle<Int>.Currency.currency(code: "jpy")

    var attributed: ToYen.AttributedStyle = AttributedStyle()

    func format(_ value: Int) -> String {
        (value * ToYen.multiplier).formatted(ToYen.formatter)
    }
}

extension ToYen {
    struct AttributedStyle: FormatStyle {
        typealias FormatInput = Int
        typealias FormatOutput = AttributedString

        func format(_ value: Int) -> AttributedString {
            (value * ToYen.multiplier).formatted(ToYen.formatter.attributed)
        }
    }
}

extension FormatStyle where Self == ToYen {
    static var toYen: ToYen { .init() }
}

30.formatted(ToYen()) // "¥3,000"
30.formatted(.toYen) // "¥3,000"
30.formatted(ToYen().attributed)
30.formatted(.toYen.attributed)

One issue

The compiler will have issue if you attempt the following:

Text(0.555, format: .percent.attributed)
error: Attributed String Support.xcplaygroundpage:38:13: error: initializer 'init(_:format:)' requires the types 'FloatingPointFormatStyle<Double>.Attributed.FormatOutput' (aka 'AttributedString') and 'String' be equivalent

This is simply because the Text(_: format:) initializer expects that the FormatOutput type associated with the passed in FormatStyle is of type String. You have to use the initializer that accepts the AttributedString type as a parameter.

Which is completely understandable, passing an unmodified AttributedString into a TextView seems of limited use.


Download the Xcode Playground with all examples

See the examples as a gist


Tags: