10

My goal is to mark all visible misspelled words in an UITextView.

The inefficient algorithm is to use the spell checker to find all ranges of misspelled words in the text, convert them to UITextRange objects using positionFromPosition:inDirection:offset etc, then get the graphics rects using the UITextInput method firstRectFromRange.

Thus all the text -> misspelled words-> NSRange collection -> UITextRange collection -> CGRect collection -> evaluate for visibility, draw visible ones

The problem is that this requires that all the text is checked, and all misspelled words are converted to graphics rects.

Thus, I imagine the way to go is to somehow find out what parts of the underlying .text in the UITextView that is visible at the moment.

Thus for range of text visible -> misspelled words-> NSRange collection -> UITextRange collection -> CGRect collection -> evaluate for visibility, draw visible ones

The code in ios - how to find what is the visible range of text in UITextView? might work as a way to bound what parts of the text to check, but still requires that all text is measured, which I imagine could be quite costly.

Any suggestions?

Community
  • 1
  • 1

2 Answers2

19

Since UITextView is a subclass of UIScrollView, its bounds property reflects the visible part of its coordinate system. So something like this should work:

- (NSRange)visibleRangeOfTextView:(UITextView *)textView {
    CGRect bounds = textView.bounds;
    UITextPosition *start = [textView characterRangeAtPoint:bounds.origin].start;
    UITextPosition *end = [textView characterRangeAtPoint:CGPointMake(CGRectGetMaxX(bounds), CGRectGetMaxY(bounds))].end;
    return NSMakeRange([textView offsetFromPosition:textView.beginningOfDocument toPosition:start],
        [textView offsetFromPosition:start toPosition:end]);
}

This assumes a top-to-bottom, left-to-right text layout. If you want to make it work for other layout directions, you will have to work harder. :)

rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • Nice. For efficiency, find end by moving length characters forwards from start - turns out that it's slow going all the way from the start in a document, but cheap to advance. – Anders Sewerin Johansen Feb 17 '12 at 10:12
  • When you say, "something like this should work", have you actually tried it? This is returning almost triple the range of the actual visual text for me. Thanks. – Casey Perkins Aug 01 '14 at 15:56
  • 1
    @JasonTyler [Here's a repo containing a test app.](https://github.com/mayoff/textview-visible-range) Works great for me. Probably requires Xcode 6. – rob mayoff Aug 01 '14 at 16:48
4

Rob's answer, written in Swift 4. I've added some safety checks.

private func visibleRangeOfTextView(textView: UITextView) -> NSRange {
    let bounds = textView.bounds
    let origin = CGPoint(x: 10, y: 10)
    guard let startCharacterRange = textView.characterRange(at: origin) else {
        return NSRange(location: 0, length: 0)
    }
    
    let startPosition = startCharacterRange.start
    let endPoint = CGPoint(x: bounds.maxX,
                           y: bounds.maxY)
    guard let endCharacterRange = textView.characterRange(at: endPoint) else {
        return NSRange(location: 0, length: 0)
    }

    let endPosition = endCharacterRange.end
    
    let startIndex = textView.offset(from: textView.beginningOfDocument, to: startPosition)
    let endIndex = textView.offset(from: startPosition, to: endPosition)
    return NSRange(location: startIndex, length: endIndex)
}

Example usage, called from a button tap:

@IBAction func buttonTapped(sender: AnyObject) {
    let range = visibleRangeOfTextView(textView: self.textView)

    // Note: "as NSString" won't work correctly with Emoji and stuff,
    // see also: http://stackoverflow.com/a/24045156/1085556
    let nsText = self.textView.text as NSString
    let text = nsText.substring(with: range)
    NSLog("range: \(range), text = \(text)")
}
Bart van Kuik
  • 4,704
  • 1
  • 33
  • 57
  • Hi Bart, do you know why when I try implementing this the first guard statement always fails, whether I make the origin as you have it, as 0, 0, or as textView.frame.minX, textView.frame.minY? – michaeldebo Jan 25 '23 at 13:44
  • @michaeldebo did you wrap the UITextView in a UIViewRepresentable? That may be the cause. – Bart van Kuik Jan 28 '23 at 23:37