Can I Stop NSViewRepresentable layout lag for NSTextView?
Asked Answered
R

1

3

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:

The first rendered frame

Frame 2:

The second rendered frame

Roseline answered 12/10, 2022 at 17:16 Comment(2)
Were you able to figure this one out yet? I have the same issue, and when this sort of problem happens inside a scrollview, it's even worse.Faldstool
It’s a bug in NSTextview I’m afraid. I opened a technical support issue with Apple.Roseline
L
1

I had a very similar problem with a ViewRepresentable on iOS where I was using CoreText to do some things with text rendering. The symptoms were nearly identical.

The solution in my case was to a override sizeThatFits() on ViewRepresentable and give it the proper information (calculating text height for frame in layout) in the source view.

func sizeThatFits(_ proposal: ProposedViewSize, uiView: <YOUR VIEW>, context: Context) -> CGSize? {
        
        return uiView.sizeThatFits(CGSize(width: proposal.width ?? 0, height: proposal.height ?? 0))
    }

I am not sure if this will work for your exact situation, but I spent a good week or so pulling my hair out on the same thing so I thought I would share.

Lanielanier answered 6/4, 2023 at 20:59 Comment(3)
Do you mean manually calculating the text size with something like this? developer.apple.com/documentation/coretext/…Faldstool
No, just merely implementing that method in that manner (again on iOS as stated) resolved this for me.Lanielanier
Just redirecting sizeThatFits didn't work for me, I had to calculate the size using CoreText, then it worked perfectly.Faldstool

© 2022 - 2024 — McMap. All rights reserved.