2

Edit

The simple solution is to move any frame calculations from viewDidLoad to viewDidAppear:


I'm having a difficult time getting the following code to work properly

The code returns the first frame for a given NSRange in a UITextView.

It works if there are no line breaks, but I get some strange behavior when I add line breaks in the UITextView.

@implementation UITextView (TextFrame)

- (CGRect)frameOfTextRange:(NSRange)range {
    UITextPosition *beginning = self.beginningOfDocument;
    UITextPosition *start = [self positionFromPosition:beginning offset:range.location];
    UITextPosition *end = [self positionFromPosition:start offset:range.length];
    UITextRange *textRange = [self textRangeFromPosition:start toPosition:end];
    CGRect rect = [self firstRectForRange:textRange];

    return [self convertRect:rect fromView:self.textInputView];
}

@end

@implementation DetailViewController

- (void)viewDidLoad 
{
    [super viewDidLoad];
   if (self.searchString) {
        CGRect rect = [self.textView frameOfTextRange:[self.textView.text rangeOfString:self.searchString]];
        ...
    }
}
chrs
  • 5,906
  • 10
  • 43
  • 74

7 Answers7

8

I found the following solution to work. Instead of using firstRectForRange:, which only returns one rect, I use selectionRectsForRange:, and then iterate over all rects and use CGRectUnion to combine them.

- (CGRect)frameOfTextRange:(NSRange)range
{
   UITextPosition *beginning = self.beginningOfDocument;
   UITextPosition *start = [self positionFromPosition: beginning offset: range.location];
   UITextPosition *end = [self positionFromPosition: start offset: range.length];

   UITextRange *textRange = [self textRangeFromPosition: start toPosition: end];

   CGRect  rect = CGRectZero;

   NSArray *selectionRects = [self selectionRectsForRange: textRange];

   for (UITextSelectionRect *selectionRect in selectionRects)
   {
      rect = CGRectUnion(rect, selectionRect.rect);
   }

   return rect;
}
koen
  • 5,383
  • 7
  • 50
  • 89
  • seems legit for optimizing it. Haven't tested, but would it make a giant rect or does it make smaller clips? – chrs Apr 13 '15 at 19:38
  • Since I'm using `CGRectUnion`, this would indeed make one giant rect. But you could of course also grab each `selectionRect.rect` and do with it what you'd like. – koen Apr 13 '15 at 19:58
  • I like your solution, so take +1 :) – chrs Apr 13 '15 at 20:03
3

Apparently it seems that firstRectForRange:is NOT working in viewDidLoad

Moving the calculations from viewDidLoadto viewDidAppear: solved the problem

Old code

- (void)viewDidLoad 
{
    [super viewDidLoad];

    if (self.searchString) {
        CGRect rect = [self.textView frameOfTextRange:[self.textView.text rangeOfString:self.searchString]];
        [self.textView scrollRangeToVisible:[self.textView.text rangeOfString:self.searchString]];
        UIView *markerView = [[UIView alloc] initWithFrame:rect];
        markerView.backgroundColor = [UIColor colorWithRed:0.810 green:0.735 blue:0.000 alpha:0.660];
        [self.textView addSubview:markerView];
        _searchString = nil;
    }
}

New and working code

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

    if (self.searchString) {
        CGRect rect = [self.textView frameOfTextRange:[self.textView.text rangeOfString:self.searchString]];
        [self.textView scrollRangeToVisible:[self.textView.text rangeOfString:self.searchString]];
        UIView *markerView = [[UIView alloc] initWithFrame:rect];
        markerView.backgroundColor = [UIColor colorWithRed:0.810 green:0.735 blue:0.000 alpha:0.660];
        [self.textView addSubview:markerView];
        _searchString = nil;
    }
}
chrs
  • 5,906
  • 10
  • 43
  • 74
3

You can force self.textView to ensure layout immediately with:

[self.textView.layoutManager ensureLayoutForTextContainer:self.textView.textContainer];

Placing that before your frameOfTextRange: call will gives you right rect.

Of course you can call that within your frameOfTextRange: method:

- (CGRect)frameOfTextRange:(NSRange)range {
    [self.layoutManager ensureLayoutForTextContainer:self.textContainer];
    ...

This makes frameOfTextRange: method can be safely called from anywhere.

rintaro
  • 51,423
  • 14
  • 131
  • 139
2

This works fine for me with line-breaks('\n') in old project.

@implementation UITextView (TextFrame)

- (CGRect)frameOfTextRange:(NSRange)range
{
    UITextPosition *beginning = self.beginningOfDocument;

    UITextPosition *start = [self positionFromPosition:beginning offset:range.location];
    UITextPosition *end = [self positionFromPosition:start offset:range.length];
    UITextRange *textRange = [self textRangeFromPosition:start toPosition:end];
    CGRect rect = [self firstRectForRange:textRange];  

    return [self convertRect:rect fromView:self.textInputView];
}

@end

Here is my Swift translation

extension UITextView {

    func frameOfTextRange(range:NSRange) -> CGRect {

        let beginning = self.beginningOfDocument;
        let start = self.positionFromPosition(beginning, offset: range.location)
        let end = self.positionFromPosition(start, offset: range.length)
        let textRange = self.textRangeFromPosition(start, toPosition: end)

        let rect = self.firstRectForRange(textRange)
        return self.convertRect(rect, fromView: self.textInputView)
    }
}
B.S.
  • 21,660
  • 14
  • 87
  • 109
  • Sorry it does exactly the same. Works fine for finding frames for words before a line break, but when I try to find the frame for a word after a line break, the frame is not correct – chrs Aug 19 '14 at 15:57
  • It wouldn't work in case I want to find the last character position and it's a new line. Tested on iOS 10.3 Simulator. – Duan Nguyen Nov 02 '17 at 03:48
1

viewDidLoad: is called right after a view is loaded from a nib or the loadView method. It's is not resized for the container it is being placed in until later.

If you were looking for the best place, it would seem to be the -[UIViewController viewDidLayoutSubviews] method:

- (void)viewDidLayoutSubviews
{
    if (self.searchString) 
    {
        CGRect rect = [self.textView frameOfTextRange:[self.textView.text rangeOfString:self.searchString]];
        [self.textView scrollRangeToVisible:[self.textView.text rangeOfString:self.searchString]];
        UIView *markerView = [[UIView alloc] initWithFrame:rect];
        markerView.backgroundColor = [UIColor colorWithRed:0.810 green:0.735 blue:0.000 alpha:0.660];
        [self.textView addSubview:markerView];
        _searchString = nil;
    }
}

You would also probably want to add a property to hold 'marketView' so that if it was laid out again (in response to rotation or something else), that it could be relocated instead of adding a new view.

Jeff Holliday
  • 760
  • 6
  • 7
0

Controllers' views in viewDidLoad have arbitrary, non-definitive frames as the parent view controllers available size, status bar status, etc., have not yet been applied.

The best place to do precise frame calculation is in viewWillAppear, that is when the loaded view has been added to the parent controllers/window view hierarchy and its frame is "definitive".

Rivera
  • 10,792
  • 3
  • 58
  • 102
0

The solution that worked for me in Swift is based on Koen's solution with one key difference. Instead of calculating the UITextRange based not the selected range I used selectedTextRange property instead. This property already returns UITextRange calculated for the selected text range.

Swift 5.3 (tested might work on lower versions):

import UIKit

extension UITextView {
    /**
     Returns selected text frame position
     */
    func frameOfSelectedTextRange() -> CGRect? {
        guard let selectedTextRange = self.selectedTextRange,
              !selectedTextRange.isEmpty else { return nil }
        let rects = self.selectionRects(for: selectedTextRange)
        
        let rect = rects.reduce(into: CGRect.zero) { (res: inout CGRect, next: UITextSelectionRect) in
            res = res.union(next.rect)
        }
        
        return rect
    }
}

Note B.S's solution uses firstRectForRange which returned to me the wrong frame position for my selected text.

a.ajwani
  • 868
  • 1
  • 8
  • 21