Search code examples
iosswiftnsattributedstringemojinsrange

Swift: NSAttributedString & emojis


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?


Solution

  • 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.