起因

使用 UITextView 实现富文本编辑器,给一行文本中局部添加不同样式,再继续输入时,局部样式会被自动覆盖。效果如下:

录屏2024-07-16 14.57.03.mov

检查 textStorage 后发现原有样式被覆盖,怀疑输入文字后,样式在某个时刻发生了改变。

寻找原因

自定义一个 testStorage,添加断点查看变更时机。代码如下:

class TextStorage: NSTextStorage {
    private let store = NSMutableAttributedString()

    override var string: String { store.string }
    
    override var mutableString: NSMutableString {
        store.mutableString
    }
    
    override func replaceCharacters(in range: NSRange, with str: String) {
        beginEditing()
        store.replaceCharacters(in: range, with: str)
        edited(.editedCharacters, range: range, changeInLength: str.count - range.length)
        endEditing()
    }

    override func setAttributes(_ attrs: [NSAttributedString.Key : Any]?, range: NSRange) {
        beginEditing()
        store.setAttributes(attrs, range: range)
        edited([.editedAttributes], range: range, changeInLength: 0)
        endEditing()
    }
    
    override func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key : Any] {
        store.attributes(at: location, effectiveRange: range)
    }
}

private let textView: UITextView = {
    // 创建自定义的 NSTextStorage
    let customTextStorage = TextStorage()
    
    // 创建 NSLayoutManager 和 NSTextContainer
    let layoutManager = NSLayoutManager()
    customTextStorage.addLayoutManager(layoutManager)
    
    let textContainer = NSTextContainer(size: .zero)
    layoutManager.addTextContainer(textContainer)
    
    return UITextView(frame: .zero, textContainer: textContainer)
}()

// 初始样式设置
textView.text = "sjldkfsldfkljl"
textView.textStorage.addAttribute(.backgroundColor, value: UIColor.purple, range: NSRange(location: 0, length: textView.text.count))
textView.textStorage.addAttribute(.backgroundColor, value: UIColor.red, range: NSRange(location: 0, length: textView.text.count - 1))

运行后,输入 o,会先执行 replaceCharacters 方法,再执行 setAttributes 方法,看起来一切正常。

Untitled

继续执行后,又执行了 replaceCharacters 方法和 setAttributes 方法,而且替换了整个文本?

Untitled

第一次执行时能看到是键盘插入了文本,应该是正常运行的。

第二次是 _performTextCheckingAnnotationOperations 方法,怀疑是哪个属性需要调整,

UITextView 的定义中查找,没有找到

Untitled

可能是在协议中定义的,顺着继承链往下找,在 UITextInputTraits 协议中找到了一个可能的属性,查看其默认值为 yes,遂将其改为 no 试试

Untitled

Untitled

  textView.spellCheckingType = .no

修改之后重新运行,发现恢复正常了?

总结

使用 UITextViewiOS 中运行时正常;使用 AppKit 中的 NSTextView 时,也正常。但使用 UITextViewMac Catalyst 环境运行时,却出现问题,一度怀疑是苹果的 BUG,没想到是特性

结论

关闭拼写检查textView.spellCheckingType = .no 后,运行正常。

textView.spellCheckingType = .no
textView.text = "sjldkfsldfkljl"
textView.textStorage.addAttribute(.backgroundColor, value: UIColor.purple, range: NSRange(location: 0, length: textView.text.count))
textView.textStorage.addAttribute(.backgroundColor, value: UIColor.red, range: NSRange(location: 0, length: textView.text.count - 1))