AttributedString: it’s a String, a Range, a Dictionary to allow type safe access of attributes. But i’m having a hard time understanding how to access with what “address/index”?

Make an AttributedString of a Measurement<UnitLength> with Measurement.FormatStyle to get an AttributedString made by Foundation.

I want to figure out how to “get at” the specific attribute ranges for styling:


        let length = Measurement<UnitLength>(value: 35, unit: .millimeters)
        
        var attrString = length.formatted(.measurement(
            width: .narrow,
            usage: .asProvided,
            numberFormatStyle: .number.precision(.fractionLength(1))).attributed)
        
        /*
         the above AttributedString contains 4 run"s
         
         String after format: 35.0mm
         35 {
         Foundation.MeasurementAttribute = value
         Foundation.NumberFormatPart = integer
         }
         . {
         Foundation.MeasurementAttribute = value
         Foundation.NumberFormatSymbol = decimalSeparator
         }
         0 {
         Foundation.MeasurementAttribute = value
         Foundation.NumberFormatPart = fraction
         }
         mm {
         Foundation.MeasurementAttribute = unit
         }
         
         they all have `Foundation.MeasurementAttribute` either .value or .unit
         */

        
        // Access the unit range. there is only one so this is what I can come up with
        // with great deal of difficulty what the key is to the attributes dictionary
        if let unitRange = attrString.runs.first(where: { run in
            // how do I know all these type scopes amd no type inference auto complete help?!
            run.attributes[AttributeScopes.FoundationAttributes.MeasurementAttribute.self] == .unit
        })?.range {
            // Apply a foreground color to the "unit" part
            attrString[unitRange].foregroundColor = .orange
        }

        // since this work:
        //  run.attributes[AttributeScopes.FoundationAttributes.MeasurementAttribute.self] == .unit
        // why this next doesn't compile
        for run in attrString.runs where run.attributes[AttributeScopes.FoundationAttributes.MeasurementAttribute.self] == .value {
            // See which run is integer, decimalSeparator or fraction?
            // help help:
            // this doesn't compile:
            // Generic parameter 'T' could not be inferred
            // Type 'AttributeScopes.FoundationAttributes' has no member 'NumberFormatPart'
            if let _ = run.attributes[AttributeScopes.FoundationAttributes.NumberFormatPart.self] {
                // apply styling here if only this compile
            }
            if let _ = run.attributes[AttributeScopes.FoundationAttributes.NumberFormatSymbol.self] {
                // 
            }
        }

My most wanted to know is how does one figure out thr API of AttributedString that work like array and dictionary?

In my simple example, how to “address” at each run and set different .foregroundColor to each?

I have a very hard time figuring first what dictionary key to use to find attributes: the isn’t appear to have any auto complete help and I must come up with the exact type hierarchy to get the right attribute key to pass to the attributes dictionary.

What’s the best way to approach this beast? It’s can be used a collection or dictionary if only I know which way and what key to use?

Try this snippet:

var copy = attrString
attrString.runs.forEach { run in
    if run.measurement == .unit {
        var attributes = run.attributes
        attributes.merge(AttributeContainer([NSAttributedString.Key.foregroundColor : NSColor.orange]))
        copy[run.range].setAttributes(attributes)
    }
}

You might be ok modifying the original string instead of a copy but I’m not sure if that’s 100% reliable.

1 Like

No macOS, I need SwiftUI and iOS. So no NS- anything.

I try using ChatGPT and after many revisition, this is what it come up with and doesn’t compile:

        // Create the AttributedString using Measurement.FormatStyle
        let length = Measurement<UnitLength>(value: 35, unit: .millimeters)
        var attrString = length.formatted(
            .measurement(width: .narrow,
                         usage: .asProvided,
                         numberFormatStyle: .number.precision(.fractionLength(1))).attributed
        )
        
        // Use AttributeContainers and replace attributes
        // compile error here: 'NumberFormatPart' is not a member type of struct 'Foundation.AttributeScopes.FoundationAttributes'
        let attributesToApply: [(field: AttributeScopes.FoundationAttributes.NumberFormatPart, color: Color)] = [
            (.integer, .teal),
            (.fraction, .purple),
            (.decimalSeparator, .red)
        ]
        
        // Replace attributes for NumberFormatPart (integer, fraction, decimal separator)
        for (field, color) in attributesToApply {
            let fieldAttribute = AttributeContainer().setting(\.numberFormatPart, to: field)
            let colorAttribute = AttributeContainer().setting(\.foregroundColor, to: color)
            attrString.replaceAttribute(fieldAttribute, with: colorAttribute)
        }
        
        // Replace attribute for the unit
        let unitAttribute = AttributeContainer().setting(\.measurementAttribute, to: .unit)
        let unitColor = AttributeContainer().setting(\.foregroundColor, to: .blue)
        attrString.replaceAttribute(unitAttribute, with: unitColor)
        
        // Print the modified AttributedString
        print(attrString)

I wish the format of the AttributedString produced by any Foundation FormatStyle are documented and show how to get at each AttributedString.Run produced by these standard FormatStyle.

The component Run’s are not documented so I had to dump the AttrStr to see what’s inside and try to reverse engineer out a way to access those Run and set attribute.

Very simple want but I can’t figure this out. I need to know how to style Date.FormatStyle and Measurement.FormatStyle AttributedString with SwiftUI.

Just instead of NSColor.orange use UIColor.orange. Still not good?

No. Changed to this:

attributes.merge(AttributeContainer([AttributedString.Key.foregroundColor : UIColor.orange]))

Doesn’t compile:

Cannot convert value of type '(Color?) -> some View' to expected dictionary key type 'NSAttributedString.Key'

Instance member 'Key' cannot be used on type 'AttributedString"; did you mean to use a value of this type instead?

Reference to member 'Key' cannot be resolved without a contextual type

It works for me as written...

example
import SwiftUI

struct ContentView: View {
    @State var label = AttributedString("Hello")
    var body: some View {
        Text(label).onAppear {
            let length = Measurement<UnitLength>(value: 35, unit: .millimeters)
            
            var attrString = length.formatted(.measurement(
                width: .narrow,
                usage: .asProvided,
                numberFormatStyle: .number.precision(.fractionLength(1))).attributed)

            var copy = attrString
            attrString.runs.forEach { run in
                if run.measurement == .unit {
                    var attributes = run.attributes
                    attributes.merge(AttributeContainer([NSAttributedString.Key.foregroundColor : UIColor.orange]))
                    copy[run.range].setAttributes(attributes)
                }
            }
            label = copy
        }
    }
}

@main struct TestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

import UIKit maybe?

1 Like

Your code compiled and the difference is you have NSAttributedString.Key.foregroundColor

I took out the NS prefix believing it for AppKit only? But i inly have

import SwiftUI

I believe this implicitly import UIKit?

Why is NS prefixed type used in iOS? Where is it come from?

Anyhow, i like to know how to “address” at the other .value runs integer, decimalSeparator and fraction part with different colors?

           attrString.runs.forEach { run in
                if run.measurement == .unit {
                    var attributes = run.attributes
                    attributes.merge(AttributeContainer([NSAttributedString.Key.foregroundColor : UIColor.orange]))
                    copy[run.range].setAttributes(attributes)
                } else if run.measurement == .value {
                    // ??
                    // here how to tell if this is integer, decimalSeparator or fraction
                    // to apply different colors?
                    var attributes = run.attributes
                    attributes.merge(AttributeContainer([NSAttributedString.Key.foregroundColor : UIColor.green]))
                    copy[run.range].setAttributes(attributes)
                }
            }

There's no strong correlation... for example NSDiffableDataSourceSnapshot is available on iOS, tvOS, etc but not macOS. NSAttributedString and its Key are part of Foundation and it is available for iOS apps if needed (like NSString, NSMutableArray etc).

For decimal point, etc, try the snippet:

                let color: UIColor? = if run.numberSymbol == .decimalSeparator {
                    .green
                } else  if run.measurement == .unit {
                    .orange
                } else if run.numberPart == .fraction {
                    .blue
                } else {
                    nil
                }
                if let color {
                    var attributes = run.attributes
                    attributes.merge(AttributeContainer([NSAttributedString.Key.foregroundColor : color]))
                    copy[run.range].setAttributes(attributes)
                }

Okay, this all works now:


    @State var label = AttributedString("Hello")
    var body: some View {
        Text(label).onAppear {
            let length = Measurement<UnitLength>(value: 35, unit: .millimeters)
            
            var attrString = length.formatted(.measurement(
                width: .narrow,
                usage: .asProvided,
                numberFormatStyle: .number.precision(.fractionLength(1))).attributed)
            
            var copy = attrString
            attrString.runs.forEach { run in
                // ??
                // here how to tell if this is integer, decimalSeparator or fraction
                // to apply different colors?
                let color: UIColor? = if run.numberSymbol == .decimalSeparator {
                    .green
                } else if run.measurement == .unit {
                    .orange
                } else if run.numberPart == .fraction {
                    .blue
                } else if run.numberPart == .integer {
                    .cyan
                } else {
                    nil
                }
                if let color {
                    var attributes = run.attributes
                    attributes.merge(AttributeContainer([NSAttributedString.Key.foregroundColor : color]))
                    copy[run.range].setAttributes(attributes)
                }
            }
            label = copy
        }
    }

Thank you very much! I wonder if this is the best way available? Is there more direct way like

attrStr.replaceAttribute(someKeyToRun, with: anAttrivuteContainer)

So i can do this:

attrStr.replaceAttribute(\.measurement.unit, with: anAttrivuteContainerOfMyColor)

attrStr.replaceAttribute(\.measurement.numberSymbol.decimalSeparator, with: anAttrivuteContainerOfMyColor)

attrStr.replaceAttribute(\.measurement.numberPart.fraction, with: anAttrivuteContainerOfMyColor)

attrStr.replaceAttribute(\.measurement.numberPart.integer, with: anAttrivuteContainerOfMyColor)

It seems the Date.FormatStyle produces an AttributedString with just simple single attribute fields. So styling each of those field is simpler. But i like better is there is common pattern for styling these AttributedString produced by Foundation.

One more q: should be able to use SwiftUI.Color with the right AttributeScope? Or must fallback to UIKit?

2 Likes

There is another way, but... is it shorter/safer/easier? :sweat_smile:

attrString.replaceAttributes(AttributeContainer([
    NSAttributedString.Key("Foundation.NumberFormatPart") :
    AttributeScopes.FoundationAttributes.NumberFormatAttributes.NumberPartAttribute.NumberPart.integer
]), with: AttributeContainer([
    NSAttributedString.Key.foregroundColor : #colorLiteral(red:0, green:1, blue: 1, alpha: 1)
]))
attrString.replaceAttributes(AttributeContainer([
    NSAttributedString.Key("Foundation.NumberFormatSymbol") :
    AttributeScopes.FoundationAttributes.NumberFormatAttributes.SymbolAttribute.Symbol.decimalSeparator
]), with: AttributeContainer([
    NSAttributedString.Key.foregroundColor : #colorLiteral(red:1, green:0, blue: 0, alpha: 1)
]))
attrString.replaceAttributes(AttributeContainer([
    NSAttributedString.Key("Foundation.NumberFormatPart") :
    AttributeScopes.FoundationAttributes.NumberFormatAttributes.NumberPartAttribute.NumberPart.fraction
]), with: AttributeContainer([
    NSAttributedString.Key.foregroundColor : #colorLiteral(red:0, green:1, blue: 0, alpha: 1)
]))
attrString.replaceAttributes(AttributeContainer([
    NSAttributedString.Key("Foundation.MeasurementAttribute") :
    AttributeScopes.FoundationAttributes.MeasurementAttribute.Component.unit
]), with: AttributeContainer([
    NSAttributedString.Key.foregroundColor : #colorLiteral(red:0, green:0, blue: 1, alpha: 1)
]))
  • I'm using color literals here instead of UIColor (don't know if there's a way to use SwiftUI.Color).
  • Hopefully there are "typo-safe" constants to use instead of hardcoded "Foundation.MeasurementAttribute", etc strings

image

1 Like

I like this version as I can see a pattern here that applies to all the AttributedString from FormatStyle’s. I think I can at least try using this method to style Date.FormatStyle.

How do you know all those String constants and those long type scoped var:

AttributeScopes.FoundationAttributes.NumberFormatAttributes.SymbolAttribute.Symbol.decimalSeparator

How did you know? Did you find this from code?

Since this kind of text styling is everywhere on l watchOS, I don’t believe they would use such tedious verbose syntax.

I the WWDC 2021 What’s New in Foundation type safe was emphasized.

Thank you very much!

Note, to this list:

I should have added: "... and faster?" †. The way it's written it has to do n passes (each replaceAttributes) through each of the r runs, so the overall time complexity is O(n*r) whereas the version above combined with the modification above has O(r) complexity. I did not test the actual performance to verify this.


  1. if you print your attributed string you'd see:
35 {
	Foundation.NumberFormatPart = integer
	Foundation.MeasurementAttribute = value
}
. {
	Foundation.NumberFormatSymbol = decimalSeparator
	Foundation.MeasurementAttribute = value
}
5 {
	Foundation.MeasurementAttribute = value
	Foundation.NumberFormatPart = fraction
}
  {
}
mm {
	Foundation.MeasurementAttribute = unit
}
  1. Then reveal the relevant swift interface (command click on some related symbol like AttributeScopes)

  2. Search nearby for one of the constants like decimalSeparator:

  1. Double check that what you found is what you want:
print("\(AttributeScopes.FoundationAttributes.NumberFormatAttributes.SymbolAttribute.Symbol.decimalSeparator)")
// decimalSeparator
  1. use that in the code and test, voi-la :slight_smile:

Yeah, it's far from ideal and few people would want that. Maybe there's something better and you and me just don't know it. It should be more like:

attrString.replaceAttributes(.init(numberFormatPart: .integer),
    with: .init(foregroundColor: color))

OTOH... the concern above (†) about quadratic time complexity raises a question: why to "prettify" (and thus promote) this way of working with attributed strings.

1 Like

The way it's written it has to do n passes (each replaceAttributes) through each of the r runs, so the overall time complexity is O(n*r)

I was hoping there is no search. Given the strong emphasis on type safe and AttributedString API based on Range, Dictionary and Array. Nested AttributedScope so Foundation, UIKit and SwiftUI can layer on top and add strong typed attributes that can be accessed directly using dictionary subscript with nice type inferred .keyPathKey. That’s what I thought should be possible from watching WWDC 2021 What’s New in Foundation.

@Tony_Parker please enlighten?

You can write this as follows (for example):

attrString.replaceAttributes(
    AttributeContainer.measurement(.value).numberSymbol(.decimalSeparator),
    with: AttributeContainer.foregroundColor(.blue)
)

Once I figured out the general builder syntax for AttributeContainer, I even got quite good code completion in Xcode.

As for coming up with this syntax, I share your frustration. Many modern Swift APIs, including AttributedString, are so dense with generics and other advanced language features that it takes a lot of very careful reading of the API reference docs to figure out how to use them.

To be fair, the AttributedString docs include several code examples that illustrate the intended usage. Here are a few links to documentation pages that have concrete examples of attributed string usage:

4 Likes

Great links, thank you!

Putting it all together
import SwiftUI

func attributedString(for number: Measurement<UnitLength>) -> AttributedString {
    number.formatted(.measurement(
        width: .narrow,
        usage: .asProvided,
        numberFormatStyle: .number.precision(.fractionLength(1))).attributed)
}

func color(for run: AttributedString.Runs.Run) -> Color? {
    if run.numberSymbol == .decimalSeparator {
        .green
    } else  if run.measurement == .unit {
        .orange
    } else if run.numberPart == .fraction {
        .blue
    } else {
        nil
    }
}

var coloredString: AttributedString {
    let length = Measurement<UnitLength>(value: 35, unit: .millimeters)
    var string = attributedString(for: length)
    string.runs.forEach { run in
        string[run.range].foregroundColor = color(for: run)
    }
    return string
}

struct ContentView: View {
    @State var label = coloredString
    var body: some View {
        Text(label)
    }
}

@main struct TestApp: App {
    var body: some Scene {
        WindowGroup { ContentView() }
    }
}
1 Like

Very nice example on how to style these AttributedString output.

2 Likes

Yeah sadly sometimes the documentation can be tricky for these types of APIs where the API interface itself is quite complex to get a really nice end-user syntax at the call site (and such is the case with AttributedString). For cases where the documentation is hard to find, it'd be super helpful if you could file a feedback to Apple mentioning what documentation you're looking for / where you couldn't find it (feel free to mention the feedback numbers here and I can help make sure the right Foundation folks see it). It'd also be great to add documentation to the format styles to indicate which attributes are produced like @tera mentioned, so we'd definitely appreciate a feedback for that as well!

@tera in the end it looks like you got to the right code and I agree I think all of that put together looks like the right way to go about it! For context, those AttributeContainer(Dictionary) APIs you were looking at are intended just for interop with NSAttributedString which uses [NSAttributedString.Key : Any] instead of AttributeContainer. The dictionary initializer allows for easily converting between the two, but if you're just working with AttributedString using the AttributeContainer type-safe APIs are the way to go via AttributeContainer.foregroundColor(.blue).numberPart(.fraction) etc. (please definitely file a feedback or open a GitHub issue on the swift repo if you're not seeing great autocomplete experiences for these APIs).

As mentioned by other here, the other APIs that might be of use to you here are:

  • The replaceAttributes(_:with:) API, for example: string.replaceAttributes(AttributeContainer.numberSymbol(.decimalSeparator), with: AttributeContainer.foregroundColor(.green))
  • The transformingAttributes API, for example:
return string.transformingAttributes(\.numberSymbol, \.measurement, \.numberPart, \.foregroundColor) {
    $3.value = if $0.value == .decimalSeparator {
        .green
    } else if $1.value == .unit {
        .orange
    } else if $2.value == .fraction {
        .blue
    } else {
        nil
    }
}

but the code you have now should work just as well!

2 Likes