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?
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.
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.FormatStyleAttributedString with SwiftUI.
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)
}
@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
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?
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:
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.
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
}
Then reveal the relevant swift interface (command click on some related symbol like AttributeScopes)
Search nearby for one of the constants like decimalSeparator:
OTOH... the concern above (†) about quadratic time complexity raises a question: why to "prettify" (and thus promote) this way of working with attributed strings.
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.
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:
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))