TextKit 1下NSTextView渲染异常(回车/退格问题)求解决方案
NSTextView(TextKit 1)渲染异常问题及解决方案
问题现象
使用TextKit 1构建的NSTextView存在以下渲染异常:
- 按下回车键时,最后一行出现卡顿且无法显示
- 按下退格键时,最后一行偶尔会出现临时重复的情况
复现代码(TextKit 1存在问题的实现)
func bug() { let textContainer = NSTextContainer() textContainer.widthTracksTextView = true textContainer.heightTracksTextView = true let layoutManager = NSLayoutManager() let textStorage = NSTextStorage(string: "") layoutManager.addTextContainer(textContainer) textStorage.addLayoutManager(layoutManager) let textView = NSTextView(frame: .zero, textContainer: textContainer) textView.autoresizingMask = [.width, .height] // 触发bug的测试文本 textView.string = "\naaaaa\nbbbbbb" textView.setSelectedRange(.init(location: 0, length: 0)) let scrollView = NSScrollView() scrollView.documentView = textView scrollView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(scrollView) NSLayoutConstraint.activate([ scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), scrollView.topAnchor.constraint(equalTo: view.topAnchor), scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) }
对比:TextKit 2无异常实现
测试发现切换到TextKit 2后,该问题完全消失,实现代码如下:
func textKit2NoBug() { let textView = NSTextView(usingTextLayoutManager: true) textView.autoresizingMask = [.width, .height] // 相同测试文本 textView.string = "\naaaaa\nbbbbbb" textView.setSelectedRange(.init(location: 0, length: 0)) let scrollView = NSScrollView() scrollView.documentView = textView scrollView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(scrollView) NSLayoutConstraint.activate([ scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), scrollView.topAnchor.constraint(equalTo: view.topAnchor), scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) }
针对TextKit 1的解决方法
由于业务场景必须依赖TextKit 1,可通过以下几种方案修复:
方案1:禁用自动高度跟踪,手动管理文本容器高度
问题根源在于NSTextContainer的heightTracksTextView属性设为true时,文本视图高度计算在编辑操作时出现异常。关闭该属性后,通过监听文本变化动态调整容器高度:
func fixedBugWithManualHeight() { let textContainer = NSTextContainer() textContainer.widthTracksTextView = true textContainer.heightTracksTextView = false // 关闭自动高度跟踪 let layoutManager = NSLayoutManager() let textStorage = NSTextStorage(string: "") layoutManager.addTextContainer(textContainer) textStorage.addLayoutManager(layoutManager) let textView = NSTextView(frame: .zero, textContainer: textContainer) textView.autoresizingMask = [.width, .height] textView.string = "\naaaaa\nbbbbbb" textView.setSelectedRange(.init(location: 0, length: 0)) // 监听文本变化,更新容器高度 textStorage.delegate = self let scrollView = NSScrollView() scrollView.documentView = textView scrollView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(scrollView) NSLayoutConstraint.activate([ scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), scrollView.topAnchor.constraint(equalTo: view.topAnchor), scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) } // MARK: - NSTextStorageDelegate extension YourViewController: NSTextStorageDelegate { func textStorage(_ textStorage: NSTextStorage, didProcessEditing editedMask: NSTextStorageEditActions, range editedRange: NSRange, changeInLength delta: Int) { guard let textView = textStorage.layoutManagers.first?.textContainers.first?.textView else { return } let usedRect = textView.layoutManager?.usedRect(for: textView.textContainer!) ?? .zero let newHeight = usedRect.height + textView.textContainerInset.top + textView.textContainerInset.bottom textView.textContainer?.size.height = newHeight textView.invalidateIntrinsicContentSize() } }
方案2:自定义TextView,强制编辑后刷新布局
如果必须保留heightTracksTextView = true,可通过重写编辑方法,在操作后强制触发布局刷新:
class FixedTextView: NSTextView { override func insertNewline(_ sender: Any?) { super.insertNewline(sender) // 强制刷新布局和显示 layoutManager?.ensureLayout(for: textContainer!) setNeedsDisplay(bounds) } override func deleteBackward(_ sender: Any?) { super.deleteBackward(sender) // 强制刷新布局和显示 layoutManager?.ensureLayout(for: textContainer!) setNeedsDisplay(bounds) } } // 使用自定义TextView替换原实现 func fixedBugWithForceRefresh() { let textContainer = NSTextContainer() textContainer.widthTracksTextView = true textContainer.heightTracksTextView = true let layoutManager = NSLayoutManager() let textStorage = NSTextStorage(string: "") layoutManager.addTextContainer(textContainer) textStorage.addLayoutManager(layoutManager) let textView = FixedTextView(frame: .zero, textContainer: textContainer) textView.autoresizingMask = [.width, .height] textView.string = "\naaaaa\nbbbbbb" textView.setSelectedRange(.init(location: 0, length: 0)) let scrollView = NSScrollView() scrollView.documentView = textView scrollView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(scrollView) NSLayoutConstraint.activate([ scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), scrollView.topAnchor.constraint(equalTo: view.topAnchor), scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) }
方案3:修正文本初始化方式
避免直接给textView.string赋值,改用NSTextStorage直接初始化文本,确保布局管理器正确处理初始排版:
func fixedBugWithProperInit() { let textContainer = NSTextContainer() textContainer.widthTracksTextView = true textContainer.heightTracksTextView = true let layoutManager = NSLayoutManager() let initialText = "\naaaaa\nbbbbbb" let textStorage = NSTextStorage(string: initialText) // 直接初始化文本 layoutManager.addTextContainer(textContainer) textStorage.addLayoutManager(layoutManager) let textView = NSTextView(frame: .zero, textContainer: textContainer) textView.autoresizingMask = [.width, .height] textView.setSelectedRange(.init(location: 0, length: 0)) let scrollView = NSScrollView() scrollView.documentView = textView scrollView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(scrollView) NSLayoutConstraint.activate([ scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), scrollView.topAnchor.constraint(equalTo: view.topAnchor), scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) }
方案说明
- 方案1:从根源解决自动高度跟踪的布局冲突,是最稳定的解决方案
- 方案2:快速修复特定编辑操作下的渲染异常,适合临时应急
- 方案3:通过修正初始化逻辑,让TextKit 1正确处理文本排版,实现成本低
内容的提问来源于stack exchange,提问作者Wojciech Kulik
相关产品推荐
相关产品推荐

