Search code examples
iosswiftuitextviewnsrange

Get the NSRange for the visible text after scroll in UITextView


I'm trying to save the location of scrolled text in a UITextView so that I can return to that location upon loading the ViewController again. I have very long strings, so I want the user to be able to scroll to a specific location and then return to that location later.

I'm using the UITextView. scrollRangeToVisible function to automatically scroll the text view, but I don't know how to get the NSRange of the text that the user is seeing. Is this the best way to go about this? I tried using the setContentOffset function but that didn't seem to do anything.

Any help is appreciated. Thanks!


Solution

  • I haven't tested this thoroughly but I believe the following should work. The APIs you need are documented in the UITextInput protocol, which UITextView adopts.

    You first need to get the UITextPosition that corresponds to a given point inside the view. You'd then convert this value into a UTF-16 character offset. For example, here I print the visible text range (in terms of UTF-16 code units) of a textView every time the view is scrolled:

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let topLeft = CGPoint(x: textView.bounds.minX, y: textView.bounds.minY)
        let bottomRight = CGPoint(x: textView.bounds.maxX, y: textView.bounds.maxY)
        guard let topLeftTextPosition = textView.closestPosition(to: topLeft),
            let bottomRightTextPosition = textView.closestPosition(to: bottomRight)
            else {
                return
        }
        let charOffset = textView.offset(from: textView.beginningOfDocument, to: topLeftTextPosition)
        let length = textView.offset(from: topLeftTextPosition, to: bottomRightTextPosition)
        let visibleRange = NSRange(location: charOffset, length: length)
        print("Visible range: \(visibleRange)")
    }
    

    In my tests, UITextView tended to count lines that were barely included in the visible range (e.g. by only one pixel), so the reported visible range tended to be one or two lines larger than what a human user would say. You may have to experiment with the exact CGPoint you pass into closesPosition(to:) to get the results you want.