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

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)
}
TextKit 2 + SwiftUI (NSViewRepresentable): NSTextLayoutManager rendering attributes don’t reliably draw/update
 
 
Q