Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NSTextStorage subclass can't handle emoji characters and changes font in some cases

I'm subclassing NSTextStorage to do some link highlighting and I've read as much as I can on the topic. Everything works fine until I type the πŸ’ emoji character.

My subclass:

private let ims = NSMutableAttributedString()

override var string: String {
    return ims.string
}

override func attributesAtIndex(location: Int, effectiveRange range: NSRangePointer) -> [String : AnyObject] {
    return ims.attributesAtIndex(location, effectiveRange: range)
}

override func replaceCharactersInRange(range: NSRange, withString str: String) {
    ims.replaceCharactersInRange(range, withString: str)
    self.edited(.EditedCharacters, range: range, changeInLength:(str as NSString).length - range.length)
}

override func setAttributes(attrs: [String : AnyObject]?, range: NSRange) {
    ims.setAttributes(attrs, range: range)
    self.edited(.EditedAttributes, range: range, changeInLength: 0)
}

Nothing complicated. Then, when entering the infamous character it switches to Courier New for some random reason:

Anything but Courier New!

Now I'm picking on the πŸ’ character, there are others that cause this maddness too. I've queried the font as I type and it goes from System > Apple Emoji > Courier New.

I've also tried setting the font from within processEditing() which semi solves the problem, It causes an extra space to be added in (not in the simulator though). And I'm hardcoding a value == bad.

Ultimate Question:

What am I doing wrong? I don't see this problem with other people's implementations where I'm certain developers have subclassed NSTextStorage.

Note: I can confirm that in objc.io's demo app the same issue is present.

like image 359
Shawn Throop Avatar asked Apr 27 '16 19:04

Shawn Throop


1 Answers

Here's my layman's understanding. Most emoji only exist in Apple's AppleColorEmoji font. When you type an emoji character, your NSTextStorage calls processEditing, which then calls fixAttributesInRange. This method ensures that any missing characters in your string are replaced with fonts that support them. If your string contains emoji, all the emoji-containing ranges will get an AppleColorEmoji font attribute.

Unfortunately, nothing stops this new font attribute from "infecting" characters typed after it. AppleColorEmoji doesn't seem to contain the usual ASCII set, so those subsequent characters get "fixed" themselves with a monospace font.

What to do about it? In my program, I want to manage the attributes for my text storage manually, since I don't want copy-and-pasted text to add new styles to my text. This means that I can simply do this:

override func setAttributes(attrs: [String : AnyObject]?, range: NSRange) {
    if self.isFixingAttributes {
        self.attributedString.setAttributes(attrs, range: range)
        self.edited(NSTextStorageEditActions.EditedAttributes, range: range, changeInLength: 0)
    }
}

override func fixAttributesInRange(range: NSRange) {
    self.isFixingAttributes = true
    super.fixAttributesInRange(range)
    self.isFixingAttributes = false
}

override func processEditing() {
    // not really fixing -- just need to make sure setAttributes follows orders
    self.isFixingAttributes = true
    self.setAttributes(nil, range: self.editedRange)
    self.setAttributes(self.dynamicType.defaultAttributes(), range: self.editedRange)
    self.isFixingAttributes = false

    super.processEditing()
}

Whenever new text is typed, I simply clear its attributes (in case any of the previously-fixed ranges "infected" it) and replace them with the default attributes. After that, super.processEditing() does its thing and fixes any new missing characters in that range (if any).

If, on the other hand, you want to be able to paste styled text into your text view, it should be possible to track your fixed ranges by comparing the before/after for fixAttributesInRange, and then preventing those styles from transferring to newly-typed text in processEditing.

like image 72
Archagon Avatar answered Oct 10 '22 22:10

Archagon