54

I need to find the pixel-frame for different ranges in a textview. I'm using the - (CGRect)firstRectForRange:(UITextRange *)range; to do it. However I can't find out how to actually create a UITextRange.

Basically this is what I'm looking for:

- (CGRect)frameOfTextRange:(NSRange)range inTextView:(UITextView *)textView {

    UITextRange*range2 = [UITextRange rangeWithNSRange:range]; //DOES NOT EXIST 
    CGRect rect = [textView firstRectForRange:range2];
    return rect;
}

Apple says one has to subclass UITextRange and UITextPosition in order to adopt the UITextInput protocol. I don't do that, but I tried anyway, following the doc's example code and passing the subclass to firstRectForRange which resulted in crashing.

If there is a easier way of adding different colored UILables to a textview, please tell me. I have tried using UIWebView with content editable set to TRUE, but I'm not fond of communicating with JS, and coloring is the only thing I need.

Thanks in advance.

Johannes Lund
  • 1,897
  • 2
  • 19
  • 32
  • You have to subclass `UITextRange` to be able to set it. The only way to set `UITextRange` properties is to access them in the subclass. It defines `start` and `end` as properties, but they can be set internally by referencing `_start` and `_end`. – Justin Feb 04 '12 at 19:28
  • Yes, but how do I then create a UITextPosition, which doesn't have any properties at all? 0.o If I subclass it as well, how could the 'firstRectForRange' know which property to use from my UITextPosition subclass? – Johannes Lund Feb 05 '12 at 00:55
  • That is something that I don't know. All I know about this is that you have to subclass to set `readonly` properties. That is why this is a comment instead of an answer. – Justin Feb 05 '12 at 01:14

7 Answers7

100

You can create a text range with the method textRangeFromPosition:toPosition. This method requires two positions, so you need to compute the positions for the start and the end of your range. That is done with the method positionFromPosition:offset, which returns a position from another position and a character offset.

- (CGRect)frameOfTextRange:(NSRange)range inTextView:(UITextView *)textView
{
    UITextPosition *beginning = textView.beginningOfDocument;
    UITextPosition *start = [textView positionFromPosition:beginning offset:range.location];
    UITextPosition *end = [textView positionFromPosition:start offset:range.length];
    UITextRange *textRange = [textView textRangeFromPosition:start toPosition:end];
    CGRect rect = [textView firstRectForRange:textRange];
    return [textView convertRect:rect fromView:textView.textInputView];
}
nebs
  • 4,939
  • 9
  • 41
  • 70
Nicolas Bachschmidt
  • 6,475
  • 2
  • 26
  • 36
  • 6
    I have a small remark. This only works if the TextField is the first responder. Otherwise all positions will be NULL and the rect has a zero size respectively. If anybody has a hint how one can get the rect without the TextField being the first responder, please share it with us. – thomas Aug 05 '12 at 17:40
  • 1
    Awesome! Helped me a lot. @thomas for me this is working without the `UITextView` being the first responder (I have a read-only `UITextView` inside a `UITableViewCell`). – mluisbrown May 05 '13 at 12:57
  • I have found the same issue as Thomas - textView.beginningOfDocument is nil if textView is not the first responder. Since everything else in this example, which works great by the way, is based off beginningOfDocument, all your calculations end up being sent to nil objects. – JasonD May 08 '13 at 21:20
  • 5
    text system is WAY over engineered – Daij-Djan Oct 07 '13 at 14:14
  • This is great, but can anyone indicate how I would get the rect of just the VISIBLE portion of this. As an example if the range is an image attachment with the top portion of the image not visible (scrolled above top border of UITextView). Is there an easy way to get the intersection (?) of the above CGRect and the UITextView's visible rect ? – Duncan Groenewald Nov 14 '13 at 02:50
  • 1
    I had a similar problem with not getting a value for `beginningOfDocument` - In my case, I had the `UITextView` as `selectable = NO`. Changing this to `YES` was required... – Gavin Hope Mar 24 '14 at 07:57
  • This works, but only if the textView/Field is the `firstResponder` (currently editing it), otherwise `beginningOfDocument` will be `nil`. As a workaround, set it as the first responder then `resignFirstResponder` ASAP. They keyboard won't flash in the UI. – Jordan H May 14 '14 at 22:49
17

It is a bit ridiculous that seems to be so complicated. A simple "workaround" would be to select the range (accepts NSRange) and then read the selectedTextRange (returns UITextRange):

- (CGRect)frameOfTextRange:(NSRange)range inTextView:(UITextView *)textView {
    textView.selectedRange = range;
    UITextRange *textRange = [textView selectedTextRange]; 
    CGRect rect = [textView firstRectForRange:textRange];
    return rect;
}

This worked for me even if the textView is not first responder.


If you don't want the selection to persist, you can either reset the selectedRange:

textView.selectedRange = NSMakeRange(0, 0);

...or save the current selection and restore it afterwards

NSRange oldRange = textView.selectedRange;
// do something
// then check if the range is still valid and
textView.selectedRange = oldRange;
auco
  • 9,329
  • 4
  • 47
  • 54
11

Swift 4 of Andrew Schreiber's answer for easy copy/paste

extension NSRange {
    func toTextRange(textInput:UITextInput) -> UITextRange? {
        if let rangeStart = textInput.position(from: textInput.beginningOfDocument, offset: location),
            let rangeEnd = textInput.position(from: rangeStart, offset: length) {
            return textInput.textRange(from: rangeStart, to: rangeEnd)
        }
        return nil
    }
}
CodenameDuchess
  • 1,221
  • 18
  • 24
6

To the title question, here is a Swift 2 extension that creates a UITextRange from an NSRange.

The only initializer for UITextRange is a instance method on the UITextInput protocol, thus the extension also requires you pass in UITextInput such as UITextField or UITextView.

extension NSRange {
    func toTextRange(textInput textInput:UITextInput) -> UITextRange? {
        if let rangeStart = textInput.positionFromPosition(textInput.beginningOfDocument, offset: location),
            rangeEnd = textInput.positionFromPosition(rangeStart, offset: length) {
            return textInput.textRangeFromPosition(rangeStart, toPosition: rangeEnd)
        }
        return nil
    }
}
Andrew Schreiber
  • 14,344
  • 6
  • 46
  • 53
0

Swift 4 of Nicolas Bachschmidt's answer as an UITextView extension using swifty Range<String.Index> instead of NSRange:

extension UITextView {
    func frame(ofTextRange range: Range<String.Index>?) -> CGRect? {
        guard let range = range else { return nil }
        let length = range.upperBound.encodedOffset-range.lowerBound.encodedOffset
        guard
            let start = position(from: beginningOfDocument, offset: range.lowerBound.encodedOffset),
            let end = position(from: start, offset: length),
            let txtRange = textRange(from: start, to: end)
            else { return nil }
        let rect = self.firstRect(for: txtRange)
        return self.convert(rect, to: textInputView)
    }
}

Possible use:

guard let rect = textView.frame(ofTextRange: text.range(of: "awesome")) else { return }
let awesomeView = UIView()
awesomeView.frame = rect.insetBy(dx: -3.0, dy: 0)
awesomeView.layer.borderColor = UIColor.black.cgColor
awesomeView.layer.borderWidth = 1.0
awesomeView.layer.cornerRadius = 3
self.view.insertSubview(awesomeView, belowSubview: textView)
VictorN
  • 11
  • 1
  • 2
0
- (CGRect)frameOfTextRange:(NSRange)range inTextView:(UITextView *)textView {
    UITextRange *textRange = [[textView _inputController] _textRangeFromNSRange:range]; // Private
    CGRect rect = [textView firstRectForRange:textRange];
    return rect;
}
Jinwoo Kim
  • 440
  • 4
  • 7
-1

Here is explain.

A UITextRange object represents a range of characters in a text container; in other words, it identifies a starting index and an ending index in string backing a text-entry object.

Classes that adopt the UITextInput protocol must create custom UITextRange objects for representing ranges within the text managed by the class. The starting and ending indexes of the range are represented by UITextPosition objects. The text system uses both UITextRange and UITextPosition objects for communicating text-layout information. There are two reasons for using objects for text ranges rather than primitive types such as NSRange:

Some documents contain nested elements (for example, HTML tags and embedded objects) and you need to track both absolute position and position in the visible text.

The WebKit framework, which the iPhone text system is based on, requires that text indexes and offsets be represented by objects.

If you adopt the UITextInput protocol, you must create a custom UITextRange subclass as well as a custom UITextPosition subclass.

For example like in those sources

Volodymyr B.
  • 3,369
  • 2
  • 30
  • 48