I use a UITextView
with text attributes editing enabled and I am having issue with getting the attributes when there are emojis in the text.
Here is the code I use:
var textAttributes = [(attributes: [NSAttributedString.Key: Any], range: NSRange)]()
let range = NSRange(location: 0, length: textView.attributedText.length)
textView.attributedText.enumerateAttributes(in: range) { dict, range, _ in
textAttributes.append((attributes: dict, range: range))
}
for attribute in textAttributes {
if let swiftRange = Range(attribute.range, in: textView.text) {
print("NSRange \(attribute.range): \(textView.text![swiftRange])")
} else {
print("NSRange \(attribute.range): cannot convert to Swift range")
}
}
And when I try it with a text like "Sample text ❤️", here is the output:
NSRange {0, 12}: Sample text
NSRange {12, 1}: cannot convert to Swift range
NSRange {13, 1}: cannot convert to Swift range
So as you can see, I cannot get the text with the emoji in it.
The text attributes are set by my custom NSTextStorage
applied on the text view. Here is the setAttributes
method:
override func setAttributes(_ attrs: [NSAttributedString.Key: Any]?, range: NSRange) {
guard (range.location + range.length - 1) < string.count else {
print("Range out of bounds")
return
}
beginEditing()
storage.setAttributes(attrs, range: range)
edited(.editedAttributes, range: range, changeInLength: 0)
endEditing()
}
Note that during editing my text view, I have some "Range out of bounds" prints.
Is there a way to convert the NSRange
to a valid Swift Range
?
The most important thing to remember when working with NSAttributedString
, NSRange
, and String
is that NSAttributedString
(and NSString
) and NSRange
are based on UTF-16 encoded lengths. But String
and its count
are based on actual character counts. They don't mix.
If you ever try to create an NSRange
with someSwiftString.count
, you will get the wrong range. Always use someSwiftString.utf16.count
.
In your specific case you are applying attributes to half for the ❤️ character due to the wrong length in the NSRange
and that cascades to the errors you see.
And in the code you posted, you need to change:
guard (range.location + range.length - 1) < string.count else {
to:
guard (range.location + range.length - 1) < string.utf16.count else {
for the same reasons described above.