I am using an NSTextView
inside NSViewRepresentable
in a SwiftUI app.
The NSViewRepresentable
correctly resizes to the height of the NSTextView, so the text flows to multiple lines, but on the first render after creation it only shows a single line.
It appears that the NSTextView
is rendered correctly on this first frame, with the text wrapped at the right point, but the NSViewRepresentable
is not displaying the full height.
The NSTextView
is created, and the text set on NSTextStorage
in the makeCoordinator()
method of NSViewRepresentable
, i.e. before NSViewRepresentable
requests the view.
I override NSTextView
to set the intrinsicContentSize
:
override func layout() {
invalidateIntrinsicContentSize()
super.layout()
}
override var intrinsicContentSize: NSSize {
layoutManager!.ensureLayout(for: textContainer!)
return CGSize(width: -1.0,
height: ceil(layoutManager!.usedRect(for: textContainer!).size.height))
}
I think this issue was hinted at by the 'bobble' mentioned in this post. The height tracking method is different but also fails to set the first frame.
Is there anything I can do to force layout sooner on the NSTextView
? The view is not fixed width so it needs to flow dynamically.
Here is a minimal reproduction:
import SwiftUI
let font = NSFont.systemFont(ofSize: 20, weight: .bold)
let testString = "This long string demonstrates how its full extent is not shown on the first frame of an NSTextView rendered within an NSViewRepresentable."
let fontAttribute = [NSAttributedString.Key.font: font]
let testText = NSAttributedString(string: testString, attributes: fontAttribute)
class InternalTextView: NSTextView
{
init() {
super.init(frame: NSRect.zero)
setContentHuggingPriority(.defaultHigh, for: .vertical)
}
override init(frame frameRect: NSRect, textContainer container: NSTextContainer?) {
super.init(frame: frameRect, textContainer: container) }
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func layout() {
invalidateIntrinsicContentSize()
super.layout()
}
override var intrinsicContentSize: NSSize {
layoutManager!.ensureLayout(for: textContainer!)
return CGSize(width: -1.0,
height: ceil(layoutManager!.usedRect(for: textContainer!).size.height))
}
}
class TextViewCoordinator: NSObject {
private(set) var view: InternalTextView
init(withText text: NSAttributedString) {
view = InternalTextView()
view.textStorage?.setAttributedString(text)
super.init()
}
}
struct TextView: NSViewRepresentable
{
let text: NSAttributedString
func makeNSView(context: Context) -> NSTextView { context.coordinator.view }
func updateNSView(_ nsView: NSTextView, context: Context) { }
func makeCoordinator() -> TextViewCoordinator { TextViewCoordinator(withText: text) }
}
struct ContentView: View
{
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
@State var showText: Bool = false
var body: some View {
HStack {
Spacer()
ScrollView {
VStack {
if showText {
ForEach(1..<10) { _ in
TextView(text: testText)
} }
Spacer()
}.frame(maxWidth: 600)
}
Spacer()
}
.onReceive(timer) { _ in
showText.toggle()
}
}
}
Frame 1:
Frame 2: