Thanks for being a part of WWDC25!

How did we do? We’d love to know your thoughts on this year’s conference. Take the survey here

Get NSTextView selection frame with NSTextLayoutManager

I'm trying to update my app to use TextKit 2. The one thing that I'm still not sure about is how I can get the selection frame. My app uses it to auto-scroll the text to keep the cursor at the same height when the text wraps onto a new line or a newline is manually inserted. Currently I'm using NSLayoutManager.layoutManager!.boundingRect(forGlyphRange:in:).

The code below almost works. When editing the text or changing the selection, the current selection frame is printed out. My expectation is that the selection frame after a text or selection change should be equal to the selection frame before the next text change. I've noticed that this is not always true when the text has a NSParagraphStyle with spacing > 0. As long as I type at the end of the text, everything's fine, but if I insert some lines, then move the selection somewhere into the middle of the text and insert another newline, the frame printed after manually moving the selection is different than the frame before the newline is inserted. It seems that the offset between the two frames is exactly the same as the paragraph style's spacing. Instead when moving the selection with the arrow key the printed frames are correct.

I've filed FB17104954.

class ViewController: NSViewController, NSTextViewDelegate {

    private var textView: NSTextView!
    
    override func loadView() {
        let scrollView = NSScrollView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
        textView = NSTextView(frame: scrollView.frame)
        textView.autoresizingMask = [.width, .height]
        textView.delegate = self
        let paragraphStyle = NSMutableParagraphStyle()
        paragraphStyle.lineSpacing = 40
        textView.typingAttributes = [.foregroundColor: NSColor.labelColor, .paragraphStyle: paragraphStyle]
        scrollView.documentView = textView
        scrollView.hasVerticalScroller = true
        view = scrollView
    }
    
    func textView(_ textView: NSTextView, shouldChangeTextIn affectedCharRange: NSRange, replacementString: String?) -> Bool {
        print("before", selectionFrame.maxY, selectionFrame)
        return true
    }
    
    func textDidChange(_ notification: Notification) {
        print("after ", selectionFrame.maxY, selectionFrame)
    }
    
    func textViewDidChangeSelection(_ notification: Notification) {
        print("select", selectionFrame.maxY, selectionFrame)
    }
    
    var selectionFrame: CGRect {
        guard let selection = textView.textLayoutManager!.textSelections.first?.textRanges.first else {
            return .null
        }
        var frame = CGRect.null
        textView.textLayoutManager!.ensureLayout(for: selection)
        textView.textLayoutManager!.enumerateTextSegments(in: selection, type: .selection, options: [.rangeNotRequired]) { _, rect, _, _ in
            frame = rect
            return false
        }
        return frame
    }

}
Get NSTextView selection frame with NSTextLayoutManager
 
 
Q