Подкласс NSTextStorage не может обрабатывать символы эмодзи и в некоторых случаях меняет шрифт.

Я создаю подкласс NSTextStorage, чтобы выделить ссылки, и я прочитал столько, сколько I можете в теме. Все работает нормально, пока я не наберу символ ???? emoji.

Мой подкласс:

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)
}

Ничего сложного. Затем при вводе печально известного персонажа он по какой-то случайной причине переключается на Courier New:

Все, кроме Курьера Новое  !

Сейчас придираюсь к персонажу ????, есть и другие, вызывающие это безумие. Я запросил шрифт по мере ввода, и он идет из «Система»> «Apple Emoji»> «Courier New».

Я также попытался установить шрифт изнутри processEditing(), что частично решает проблему, это приводит к добавлению дополнительного пробела (но не в симуляторе). И я жестко задаю значение == плохое.

Окончательный вопрос:

Что я делаю не так? Я не вижу этой проблемы с реализациями других людей, где, я уверен, разработчики создали подкласс NSTextStorage.

Примечание. Я могу подтвердить, что в демонстрационном приложении objc.io присутствует та же проблема.


person Shawn Throop    schedule 27.04.2016    source источник
comment
Привет. Вы поняли, что происходит? Я просто столкнулся с той же проблемой и не могу найти хорошее решение.   -  person Nekto    schedule 21.06.2016
comment
@Nekto Лучшее, что мне удалось сделать, это убедиться, что атрибут attributeString/textStorage имеет значение для ключа NSFontAttributeName. Я уверен в этом в processEditing()   -  person Shawn Throop    schedule 21.06.2016
comment
Я понимаю. В итоге я заставил шрифт, который мне нужен, и в processEditing. По крайней мере, это не ломает смайлики.   -  person Nekto    schedule 21.06.2016
comment
Как ни странно, даже полностью стандартный NSTextView, похоже, имеет такое поведение. Если вы наберете эмодзи, все, что будет после него, станет красным моноширинным шрифтом. fixAttributesInRange(range: NSRange) — это место, где этот материал определяется в NSTextStorage, так что это, вероятно, лучшее место для обхода этого. К сожалению, простое удаление любых нераспознанных шрифтов не во всех случаях будет работать, так как fixAttributesInRange также выполняет замену шрифта для неизвестных символов (как это делается для эмодзи).   -  person Archagon    schedule 12.09.2016
comment
О, я вижу, что происходит. Когда вы вводите смайлик, диапазон шрифта для этого смайлика преобразуется (на fixAttributesInRange) в шрифт эмодзи Apple (AppleColorEmoji). Это означает, что все, что напечатано после этого смайлика, также будет использовать этот шрифт (как и ожидалось с атрибутами NSAttributedString). Однако AppleColorEmoji не содержит символов для обычных букв, поэтому диапазон этих букв, в свою очередь, изменяется с AppleColorEmoji на моноширинный. Если бы только был способ указать NSAttributedString ограничить шрифт только определенным диапазоном и никогда не увеличивать его!   -  person Archagon    schedule 12.09.2016
comment
@ShawnThroop, ты когда-нибудь это понимал? Реализация fixAttributes(in range: NSRange) у меня не сработала. Я все еще печатаю _NSLayoutTreeLineFragmentRectForGlyphAtIndex invalid glyph index 1 в отладчике с эмодзи   -  person tettoffensive    schedule 21.04.2017
comment
@tettoffensive, в конце концов, я добавил словарь defaultAttributes (типа [String: Any]), и в processEditing() я применяю эти атрибуты к значению диапазона, возвращаемому paragraphRange(for: editedRange), до применения других атрибутов. В рамках init я присваиваю значение шрифта, созданное из API UIFontDescriptor, атрибуту defaultAttributes и через didSet вызываю edited(_: range: changeInLength:), передавая .editedAttributes для editedMask.   -  person Shawn Throop    schedule 21.04.2017


Ответы (3)


Вот мое непрофессиональное понимание. Большинство эмодзи существуют только в шрифте AppleColorEmoji от Apple. Когда вы вводите символ эмодзи, ваш NSTextStorage вызывает processEditing, который затем вызывает fixAttributesInRange. Этот метод гарантирует, что любые отсутствующие символы в вашей строке будут заменены шрифтами, которые их поддерживают. Если ваша строка содержит эмодзи, все диапазоны, содержащие эмодзи, получат атрибут шрифта AppleColorEmoji.

К сожалению, ничто не мешает этому новому атрибуту шрифта «заражать» символы, напечатанные после него. AppleColorEmoji, похоже, не содержит обычного набора ASCII, поэтому эти последующие символы «исправляются» с помощью моноширинного шрифта.

Что с этим делать? В моей программе я хочу вручную управлять атрибутами для моего текстового хранилища, так как я не хочу, чтобы текст с копированием и вставкой добавлял новые стили к моему тексту. Это означает, что я могу просто сделать это:

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()
}

Всякий раз, когда набирается новый текст, я просто очищаю его атрибуты (на случай, если какой-либо из ранее фиксированных диапазонов «заразил» его) и заменяю их атрибутами по умолчанию. После этого super.processEditing() делает свое дело и исправляет любые новые отсутствующие символы в этом диапазоне (если они есть).

Если, с другой стороны, вы хотите иметь возможность вставлять стилизованный текст в свое текстовое представление, должна быть возможность отслеживать ваши фиксированные диапазоны, сравнивая до/после для fixAttributesInRange, а затем предотвращая перенос этих стилей в новый тип. текст в processEditing.

person Archagon    schedule 12.09.2016
comment
Или, я думаю, просто иметь строку с нефиксированными атрибутами, которую вы фактически редактируете, и отображаемую строку с атрибутами (или какую-либо другую структуру данных для хранения атрибутов), которая улавливает все фиксированные атрибуты из fixAttributesInRange. - person Archagon; 17.09.2016
comment
что такое self.dynamicType у моего NSTextStorage, похоже, нет этого - person tettoffensive; 21.04.2017
comment
Он устарел: stackoverflow.com/questions/39495021 / - person Archagon; 21.04.2017
comment
@tettoffensive self.dynamicType — это старая версия функции Swift type(of: ). Подобно [self class] в Objective-C. - person Shawn Throop; 21.04.2017

На самом деле оказывается, что в моем случае мне просто нужно было изменить:

self.edited(.editedCharacters, range: range, changeInLength: str.characters.count-range.length)

to:

self.edited(.editedCharacters, range: range, changeInLength: (str as NSString).length-range.length)

К сожалению, получение длины строки отличается от NSString.

fixAttributes(in range: NSRange) был НЕ нужен

person tettoffensive    schedule 20.04.2017
comment
Это вводящий в заблуждение ответ, потому что он касается совершенно другого вопроса, связанного с различиями в том, как Swift.String и NSString вычисляют длину строки. tl;dr: это довольно сложно. - person Shawn Throop; 21.04.2017
comment
Тип Swift String имеет свойство utf16 именно по этой причине. Просто примите это во внимание, и вам не придется связывать строку с миром Objective-C. - person Vitalii Vashchenko; 10.09.2017

Я исследовал этот вопрос в течение нескольких часов. Итак, в заключение, вставка (набор или вставка) символа эмодзи или размещение курсора после некоторых символов эмодзи (например, ☺️) приводило к изменению шрифта при наборе текста на «AppleColorEmoji», который в конечном итоге возвращался к «Courier New", когда вставлен символ, не являющийся смайликом. Это происходит только в том случае, если используется подкласс NSTextStorage, в противном случае шрифт ввода никогда не менялся на «AppleColorEmoji». Итак, исправляем, сбросив шрифт набора из AppleColorEmoji на шрифт по умолчанию, установленный разработчиком. Исправление применяется до и после вставки текста. Первый исправляет изменение шрифта при наборе текста из-за того, что курсор помещается после символа эмодзи, а второй исправляет изменение шрифта при наборе текста из-за вставки символа эмодзи (изменения шрифта при наборе каким-то образом отражаются в параметре UITextView.font).

См. https://github.com/CosmicMind/Material/pull/1117.

class EmojiFixedTextView: UITextView {
    private var _font: UIFont?

    override var font: UIFont? {
        didSet {
            _font = font
        }
    }

    override func insertText(_ text: String) {
        fixTypingFont()
        super.insertText(text)
        fixTypingFont()
    }

    override func paste(_ sender: Any?) {
        fixTypingFont()
        super.paste(sender)
        fixTypingFont()
    }

    private func fixTypingFont() {
        let fontAttribute = NSAttributedStringKey.font.rawValue
        guard (typingAttributes[fontAttribute] as? UIFont)?.fontName == "AppleColorEmoji" else {
            return
        }

        typingAttributes[fontAttribute] = _font
    }
}
person Orkhan Alikhanov    schedule 18.07.2018