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