You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 08:54:57