TextKit 2 + SwiftUI (NSViewRepresentable): NSTextLayoutManager rendering attributes don’t reliably draw/update

I’m embedding an NSTextView (TextKit 2) inside a SwiftUI app using NSViewRepresentable. I’m trying to highlight dynamic subranges (changing as the user types) by providing per-range rendering attributes via NSTextLayoutManager’s rendering-attributes mechanism.

The issue: the highlight is unreliable.

  • Often, the highlight doesn’t appear at all even though the delegate/data source is returning attributes for the expected range.

  • Sometimes it appears once, but then it stops updating even when the underlying “highlight range” changes.

This feels related to SwiftUI - AppKit layout issue when using NSViewRepresentable (as said in https://developer.apple.com/documentation/swiftui/nsviewrepresentable).

What I’ve tried

  • Updating the state that drives the highlight range and invalidating layout fragments / asking for relayout

  • Ensuring all updates happen on the main thread.

  • Calling setNeedsDisplay(_:) on the NSViewRepresentable’s underlying view.

  • Toggling the SwiftUI view identity (e.g. .id(...)) to force reconstruction (works, but too expensive / loses state).

Question

In a SwiftUI + NSViewRepresentable setup with TextKit 2, what is the correct way to make NSTextLayoutManager re-query and redraw rendering attributes when my highlight ranges change?

  • Is there a recommended invalidation call for TextKit 2 to trigger re-rendering of rendering attributes?

  • Or is this a known limitation when hosting NSTextView inside SwiftUI, where rendering attributes aren’t reliably invalidated?

  • If this approach is fragile, is there a better pattern for dynamic highlights that avoids mutating the attributed string (to prevent layout/scroll jitter)?

Answered by DTS Engineer in 879372022

Thanks for sharing more information. Even without SwiftUI, addRenderingAttribute + invalidateLayout doesn't refresh the text view (NSTextView) either (though the text view does refresh with the right rendering attribute after I resize the app window), which I believe is worth a feedback report, and would suggest that you file one and share your report ID here.

Before the issue is addressed, a workaround I can see is to use display attributes for that purpose – When loading the the text storage, UI/NSTextView calls textContentStorage(_:textParagraphWith:), and you can implement the method to set up display attributes for the text coloring, and return an appropriate NSTextParagraph, as shown in the following code example:

extension ViewController: NSTextContentStorageDelegate {
    func textContentStorage(_ textContentStorage: NSTextContentStorage, textParagraphWith range: NSRange) -> NSTextParagraph? {
        let paragraphText = textContentStorage.textStorage!.attributedSubstring(from: range)
        let paragraphString = paragraphText.string
        let keywords = ["keyword1"]
        
        let displayAttributes: [NSAttributedString.Key: AnyObject] = [
            .font: NSFont.preferredFont(forTextStyle: .title3, options: [:]),
            .foregroundColor: NSColor.red
        ]
        
        let textWithDisplayAttributes = NSMutableAttributedString(attributedString: paragraphText)
        paragraphString.enumerateSubstrings(in: paragraphString.startIndex..<paragraphString.endIndex,
                                            options: .byWords) { substring, substringRange, enclosingRange, stop in
            defer { stop = false}
            guard let substring = substring, keywords.contains(substring) else { return }
            let start = paragraphString.distance(from: paragraphString.startIndex, to: substringRange.lowerBound)
            let length = paragraphString.distance(from: substringRange.lowerBound, to: substringRange.upperBound)
            
            let rangeForDisplayAttributes = NSRange(location: start, length: length)
            textWithDisplayAttributes.addAttributes(displayAttributes, range: rangeForDisplayAttributes)
        }
        return NSTextParagraph(attributedString: textWithDisplayAttributes)
    }
}

I believe this should work and have good enough performance, but let me know if it doesn't.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

I'd like to start with pointing you the following pure SwiftUI TextEditor sample, which may be good enough if your intent is to highlight a range of text (and do basic text editing) in a SwiftUI app:

If you do need TextKit 2 for other reasons, I'd suggest that you provide a minimal project that demonstrates the issue for folks to take a closer look.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Thanks for the quick response.

I’d prefer to use a pure SwiftUI text solution, but at the moment I need TextKit 2 for features SwiftUI still doesn’t expose—real-time syntax highlighting, paragraphStyle handling, tracking visibleRect changes, and related editor behaviors.

Below is a minimal example showing how I’m using NSTextLayoutManager rendering attributes for a dynamic highlight.

import SwiftUI
struct RATestView: View {
    @State private var text = """
        TextKit 2 rendering highlight demo.
        Type something and watch highlight update.
    """
    @State private var search = "highlight"
    var body: some View {
        VStack {
            TextField("Search", text: $search)
            WrapperView(text: $text, highlight: search)
                .frame(height: 300)
        }
    }
}
private struct WrapperView: NSViewRepresentable {
    @Binding var text: String
    var highlight: String
    func makeNSView(context: Context) -> CustomTextView {
        let view = CustomTextView()
        return view
    }
    func updateNSView(_ nsView: CustomTextView, context: Context) {
        nsView.setText(text)
        nsView.setHighlight(highlight)
    }
}
private final class CustomTextView: NSView {
    private let textView = NSTextView()
    override init(frame frameRect: NSRect) {
        super.init(frame: frameRect)
        setupView()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupView()
    }
    private func setupView() {
        addSubview(textView)
        textView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            textView.leadingAnchor.constraint(equalTo: leadingAnchor),
            textView.trailingAnchor.constraint(equalTo: trailingAnchor),
            textView.topAnchor.constraint(equalTo: topAnchor),
            textView.bottomAnchor.constraint(equalTo: bottomAnchor)
        ])
    }
    func setText(_ string: String) {
        if textView.string != string { textView.string = string }
    }
    func setHighlight(_ highlightString: String) {
        guard let documentRange = textView.textLayoutManager?.documentRange else { return }
        textView.textLayoutManager?.invalidateRenderingAttributes(for: documentRange)
        let highlightRange = (textView.string as NSString).range(of: highlightString)
        guard let range = convertToTextRange(textView: textView, range: highlightRange) else { return }
        textView.textLayoutManager?.addRenderingAttribute(.backgroundColor, value: NSColor(.red), for: range)
        textView.textLayoutManager?.invalidateLayout(for: range)
        textView.needsDisplay = true
        textView.needsLayout = true
    }
}
private func convertToTextRange(textView: NSTextView, range: NSRange) -> NSTextRange? {
    guard let textLayoutManager = textView.textLayoutManager,
          let textContentManager = textLayoutManager.textContentManager,
          let start = textContentManager.location(textContentManager.documentRange.location, offsetBy: range.location),
          let end = textContentManager.location(start, offsetBy: range.length)
    else { return nil }
    return NSTextRange(location: start, end: end)
}

Thanks for sharing more information. Even without SwiftUI, addRenderingAttribute + invalidateLayout doesn't refresh the text view (NSTextView) either (though the text view does refresh with the right rendering attribute after I resize the app window), which I believe is worth a feedback report, and would suggest that you file one and share your report ID here.

Before the issue is addressed, a workaround I can see is to use display attributes for that purpose – When loading the the text storage, UI/NSTextView calls textContentStorage(_:textParagraphWith:), and you can implement the method to set up display attributes for the text coloring, and return an appropriate NSTextParagraph, as shown in the following code example:

extension ViewController: NSTextContentStorageDelegate {
    func textContentStorage(_ textContentStorage: NSTextContentStorage, textParagraphWith range: NSRange) -> NSTextParagraph? {
        let paragraphText = textContentStorage.textStorage!.attributedSubstring(from: range)
        let paragraphString = paragraphText.string
        let keywords = ["keyword1"]
        
        let displayAttributes: [NSAttributedString.Key: AnyObject] = [
            .font: NSFont.preferredFont(forTextStyle: .title3, options: [:]),
            .foregroundColor: NSColor.red
        ]
        
        let textWithDisplayAttributes = NSMutableAttributedString(attributedString: paragraphText)
        paragraphString.enumerateSubstrings(in: paragraphString.startIndex..<paragraphString.endIndex,
                                            options: .byWords) { substring, substringRange, enclosingRange, stop in
            defer { stop = false}
            guard let substring = substring, keywords.contains(substring) else { return }
            let start = paragraphString.distance(from: paragraphString.startIndex, to: substringRange.lowerBound)
            let length = paragraphString.distance(from: substringRange.lowerBound, to: substringRange.upperBound)
            
            let rangeForDisplayAttributes = NSRange(location: start, length: length)
            textWithDisplayAttributes.addAttributes(displayAttributes, range: rangeForDisplayAttributes)
        }
        return NSTextParagraph(attributedString: textWithDisplayAttributes)
    }
}

I believe this should work and have good enough performance, but let me know if it doesn't.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

TextKit 2 &#43; SwiftUI (NSViewRepresentable): NSTextLayoutManager rendering attributes don’t reliably draw/update
 
 
Q