Search code examples
swiftuiuitextviewnsattributedstringnstextview

Get X/Y position of substring in multi-line + scrollable TextView in SwiftUI and macOS 11


Plan

I have implemented a TextView into my Swift-app (macOS 11) using the following implementation, which makes the TextView scrollable by placing it into a ScrollViewer:

https://gist.github.com/unnamedd/6e8c3fbc806b8deb60fa65d6b9affab0

Now, I'm trying to display annotations on the side of the text, which should eventually look something like this:

Annotations

I planned to achieve this by

  1. Setting a custom AttributedString-key to the NSAttributedString of the TextView
  2. Add a second ScrollView on the left side, which contains the annotations, and synchronise the scroll position of both ScrollViews (i.e., the left one and the one in the TextView.)
  3. Calculate the Y-position of each annotation using the relevant substring and place/draw the annotation/connecting line

Problem

I tried to evaluate the Y-Position of a substring using the textView.firstRect function (which I found in all the other, old SO-questions), however, it does not seem to return the correct values unless the position of a substring within the first line of the TextView is used.

    class TextViewExtended: NSTextView {    

func getAllRectangles() {
    for CurRange in self.selectedRanges {
        self.layoutManager!.ensureLayout(for: self.textContainer!) // S. https://stackoverflow.com/a/30752520/2624880
        let CurRangeAsNSRange:NSRange = CurRange as! NSRange
        var actualRange = NSRange(location: 0, length: self.attributedString().length)
        let rect = self.firstRect(forCharacterRange: CurRangeAsNSRange, actualRange: &actualRange)
    }}}

Question

What would be to correct way to determine the coordinates of a substring of a TextView in SwiftUI & macOS 11? Or does anybody have an even simpler idea how I could achieve this?

Thanks a lot in advance for any advise!

Ps. I have omitted the full code of the TextView implementation since it follows the above tutorial closely; please let me know if I should add additional code.


Solution

  • Try updating the rect using the actualRange. some thing like this:

    rect = textView.firstRect(forCharacterRange: range, actualRange: &actualRange)
    while actualRange.upperBound < rangeToCheck.upperBound {
        rangeToCheck.length = (rangeToCheck.upperBound - actualRange.upperBound)
        rangeToCheck.location = actualRange.upperBound
        rect = textView.firstRect(forCharacterRange: rangeToCheck, actualRange: &actualRange)
    }