1
votes

I'm having a trouble with emoji in a custom NSTextStorage subclass. The class does not store any attributes passed to it. Instead, it generates its own:

override func attributesAtIndex(location: Int, effectiveRange range: NSRangePointer) -> [String : AnyObject] {
    if range != nil {
        range.memory = NSMakeRange(0, self.string.length)
    }

    let attributes = [
        NSFontAttributeName: NSFont.systemFontOfSize(14)
    ]

    return attributes
}

override func setAttributes(attrs: [String : AnyObject]?, range: NSRange) {
    // does nothing
}

This mostly works fine. However, if there are any emoji in the string, they simply don't show up. In examining the calls that the NSTextView makes to the text storage, it appears that the text view tries to set the font attribute of any emoji ranges to the AppleColorEmoji font whenever they appear. That's fine if you're relying on the text view as the source of "attribute truth", but I don't want my program to work like that. The text storage, in my case, needs to be the sole vendor of any attributes. It can't listen to anything that the text view sends it, attribute-wise.

Am I going to have to manually detect any emoji in my string and set the AppleColorEmoji font manually? Or is there a better way? I've already tried using fallback fonts and automatically searching for fonts that contain missing characters, but emoji don't seem to be covered using those methods.

1

1 Answers

5
votes

Figured it out. In short, attributed strings (and consequently text storages) call fixAttributesInRange(range: NSRange) after editing in order to tidy up the attributes for presentation, e.g. adding the emoji font where needed. fixAttributesInRange, in turn, calls setAttributes(attrs: [String : AnyObject]?, range: NSRange) to commit these extra attributes. This means you can't just vend a locally-constructed dictionary out of attributesAtIndex(location: Int, effectiveRange range: NSRangePointer): you have to keep track of these "fixed" attributes or your string will break in certain situations.

Unfortunately, setAttributes also receives attributes from the text view, which we want to ignore. Fortunately, it's easy to get around this by doing this:

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

...and then checking for the isFixingAttributes flag in setAttributes. That way, only "fixed" attributes will be recorded by the text storage, and not any that come in from the outside.