1
votes

I have set up a simple playground to demonstrate my problem in retrieving the attributes of attributed strings. Perhaps I do not understand how to define ranges: perhaps I am missing something else.

I have define a NSMutableAttributedString that has two colors in it as well as a change in font to bold(for the blue) and italic(for the red):

var someText =  NSMutableAttributedString()

let blueAttri : [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 12), NSAttributedString.Key.foregroundColor : UIColor.blue]
let redAttri : [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: UIFont.italicSystemFont(ofSize: 12), NSAttributedString.Key.foregroundColor : UIColor.red]

someText = NSMutableAttributedString(string: "Some blue string here; ", attributes: blueAttri)
someText.append(NSMutableAttributedString(string: "Some red string here; ", attributes: redAttri))

At this point the string looks fine showing text with both colors.

I then have defined 3 ranges (attempting different approaches to defining ranges.)

var range1 = NSRange()
var range2 = NSRange(location: 0, length: someText.length)
var range3 = NSRange(location: 40, length: 44)

Lastly, I try to retrieve attributes for the text

// retrieve attributes
let attributes1 = someText.attributes(at: 0, effectiveRange: &range1)

// iterate each attribute
print("attributes1")
for attr in attributes1 {
    print(attr.key, attr.value)
}

let attributes2 = someText.attributes(at: 0, effectiveRange: &range2)

// iterate each attribute
print("attributes2")
for attr in attributes2 {
    print(attr.key, attr.value)
}


let attributes3 = someText.attributes(at: 0, effectiveRange: &range3)

// iterate each attribute
print("attributes3")
for attr in attributes3 {
    print(attr.key, attr.value)
}

I get the following results. In all cases showing only the first set of attributes.

attributes1
NSAttributedStringKey(_rawValue: NSColor) UIExtendedSRGBColorSpace 0 0 1 1
NSAttributedStringKey(_rawValue: NSFont) font-family: ".SFUIText-Semibold"; font-weight: bold; font-style: normal; font-size: 12.00pt

attributes2
NSAttributedStringKey(_rawValue: NSColor) UIExtendedSRGBColorSpace 0 0 1 1 NSAttributedStringKey(_rawValue: NSFont) font-family: ".SFUIText-Semibold"; font-weight: bold; font-style: normal; font-size: 12.00pt

attributes3
NSAttributedStringKey(_rawValue: NSColor) UIExtendedSRGBColorSpace 0 0 1 1
NSAttributedStringKey(_rawValue: NSFont) font-family: ".SFUIText-Semibold"; font-weight: bold; font-style: normal; font-size: 12.00pt

What do I need to do to get all of the attributes in the string?

It has been suggested I use enumerate attributes. That does not seem to be legal. See below: enter image description here

2
It's the attributes(at:, effectiveRange:) method that you didn't understand. It take the attributes at the place at, and with effectiveRange, that's something else. So at 0, for the first two tries, you get the same, that's normal. To get all the attributes, use enumerateAttributes(in:options:using:) where you set the range from 0 to the length.Larme
enumerateAttributes does not appear possible. Please see image added above.jz_
It's possible. Try it. But it doesn't return a value.rmaddy
It doesn't return anything, that's why it's not allowed in your case. You can't do let attribute4 = someText.enumerateAttributes(in:options:using:), but you can do someText.enumerateAttributes(in:options:using:)Larme
Also, that's in Objective-C, but logic is the same (stackoverflow.com/questions/32297969/…) but the range parameter is a pointer, meaning you don't put value in it, it will have afterwards the range corresponding.Larme

2 Answers

2
votes

Using enumerateAttributes I was able to capture ranges with Italic (just to show one example). The following is the complete code:

//Create Empty Dictionaries for storing results
var attributedFontRanges = [String: UIFont]()
var attributedColorRanges = [String: UIColor]()

//Find all attributes in the text.
someText.enumerateAttributes(in: NSRange(location: 0, length: someText.length)) { (attributes, range, stop) in

    attributes.forEach { (key, value) in
        switch key {

        case NSAttributedString.Key.font:
            attributedFontRanges[NSStringFromRange(range)] = value as? UIFont

        case NSAttributedString.Key.foregroundColor:
            attributedColorRanges[NSStringFromRange(range)] = value as? UIColor

        default:

            assert(key == NSAttributedString.Key.paragraphStyle, "Unknown attribute found in the attributed string")
        }
}
}

//Determine key(range) of Italic font
var italicRange = attributedFontRanges.filter { $0.value.fontName == ".SFUIText-Italic" }.keys

print("italicRange: \(italicRange)")

The following results are printed: italicRange: ["{23, 22}"]

1
votes

You are using attributes with the same at: value of 0 for all three calls. That returns the attributes for the 1st character in the string.

If you want all of the attributes in a range, use enumerateAttributes and pass in the range you want to iterate over.

Note: The range you pass needs to be based on the UTF-16 encoding of the string, not the Swift string length. Those two lengths can be different when you have special characters such as Emojis. Using NSAttributedString length is fine.