Create UITextRange from NSRange

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:

  • UITextView style is being reset after setting text property
  • Core Data iPhone - Save/Load depending on Date
  • How to make an expression clickable on iOS?
  • UITextView resign first responder on 'Done'
  • How to do a live UITextField count while typing (Swift)?
  • Justified text with UITextView and NSMutableAttributedString
  • - (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.

    5 Solutions Collect From Internet About “Create UITextRange from NSRange”

    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];
    }
    

    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;
    

    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
        }
    }
    

    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
        }
    }
    

    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