7

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?

Community
  • 1
  • 1
Florentin
  • 1,433
  • 2
  • 13
  • 22
  • *"Is there a way to convert the NSRange to a valid Swift Range?"* - you are doing that with `Range(attribute.range, in: textView.text)`. – rmaddy Jun 06 '19 at 04:18
  • 1
    `guard (range.location + range.length - 1) < string.count else {` needs to be `guard (range.location + range.length - 1) < string.utf16.count else {`. – rmaddy Jun 06 '19 at 04:18
  • @rmaddy Thank you, the `string.utf16.count` worked! However, the conversion to Swift `Range` still write "cannot convert to Swift range". I succeeded to substring with `textView.attributedText.attributedSubstring(from: attribute.range).string` – Florentin Jun 06 '19 at 04:31
  • 1
    Somewhere you have an `NSRange` that is probably based on a Swift `String` count and that is wrong. It must always be `.utf16.count` when creating an `NSRange` from a `String`. So again, show how you apply the attributes to the attributed text (and show how you create the ranges). – rmaddy Jun 06 '19 at 04:34
  • Thank you very much, I changed all my `.count` to `.utf16.count` in `NSRange` and it works as expected! – Florentin Jun 06 '19 at 04:48

1 Answers1

17

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.

rmaddy
  • 314,917
  • 42
  • 532
  • 579