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.
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:
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.
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.
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
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With