Why NSTextView scrolls to bottom when loaded asynchronously?

127 Views Asked by At

I have a simple Swift code for a macOS app:

// ViewController.swift

import Cocoa

let string = (1...30).map { "line \($0)" }.joined(separator: "\n")

class ViewController: NSViewController {
  private func setupTextView() {
    let scrollView = NSTextView.scrollableTextView()
    guard let textView = scrollView.documentView as? NSTextView else {
      return
    }
    
    textView.string = string
    
    /**
     * If I comment the following line, it won't scroll.
     */
    textView.layoutManager?.allowsNonContiguousLayout = true
    
    view.addSubview(scrollView)
    scrollView.translatesAutoresizingMaskIntoConstraints = false
    
    NSLayoutConstraint.activate([
      view.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
      view.topAnchor.constraint(equalTo: scrollView.topAnchor),
      view.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
      view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
    ])
  }
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    /**
     * If I execute self.setupTextView() directly without DispatchQueue.main.async, it won't scroll.
     */
    DispatchQueue.main.async {
      self.setupTextView()
    }
  }
}

When the app is starts, the text view automatically scrolls to the bottom, contrary to my intent for it to remain unscrolled so that the first line is visible.

Interestingly, if I commented out textView.layoutManager?.allowsNonContiguousLayout = true, or moved self.setupTextView() out of the async block, it worked as expect.

Just wanted to understand what is the root cause of the scrolling, and what's the best practice to avoid it (given I need to set it up async and allowsNonContiguousLayout is required)?

3

There are 3 best solutions below

2
Willeke On

scrollView.frame.size is (width = 0, height = 0). The number of lines and scroll position can't be calculated correctly. When the scroll view is resized later, the scroll position remains incorrect.

I think the Non Contiguous Layout calculates the number of lines differently.

The async block adds the scroll view when its superview is visible. I think the frame sizes, number of lines and scroll position are calculated in a different order.

Solution: Set the frame size of the scroll view to a realistic value. For example in this case

scrollView.frame = view.bounds

or

scrollView.frame.size = NSSize(width: 100, height: 100)

0
Mojtaba Hosseini On

You can put the allowsNonContiguousLayout into a dedicated async block to prevent the unwanted scroll without commenting it out:

DispatchQueue.main.async {
    textView.layoutManager?.allowsNonContiguousLayout = true
}

The rest of the code will be the same.

0
Mohamed Azarudeen Z On

let meo try. to avoid this issue, you can modify the setup that the allowsNonContiguousLayout property is set before the text view is laid out.try setting up the text view and configuring its properties synchronously, without relying on DispatchQueue.main.async. like this.

override func viewDidLoad() {
    super.viewDidLoad()
    
    // Synchronously set up the text view and configure its properties
    self.setupTextView()
}

By setting up the text view synchronously,the allowsNonContiguousLayout property is applied before the layout process begins, preventing the unexpected scrolling behavior hopefully.