26

Is there a way of getting the position (CGPoint) of the cursor (blinking bar) in an UITextView (preferable relative to its content). I don’t mean the location as an NSRange. I need something around:

- (CGPoint)cursorPosition;

It should be a non-private API way.

Suragch
  • 484,302
  • 314
  • 1,365
  • 1,393
Raphael Schaad
  • 1,659
  • 1
  • 17
  • 17

6 Answers6

49

Requires iOS 5

CGPoint cursorPosition = [textview caretRectForPosition:textview.selectedTextRange.start].origin;

Remember to check that selectedTextRange is not nil before calling this method. You should also use selectedTextRange.empty to check that it is the cursor position and not the beginning of a text range. So:

if (textview.selectedTextRange.empty) {
    // get cursor position and do stuff ...
}
Craz
  • 8,193
  • 2
  • 23
  • 16
  • 3
    This is iOS 5 only. Although -caretRectForPosition is available 3.2 and later, UITextView didn't conform to UITextInput until iOS 5 – Jonathan Dann Jan 06 '12 at 03:24
14

SWIFT 4 version:

if let cursorPosition = textView.selectedTextRange?.start {
    // cursorPosition is a UITextPosition object describing position in the text (text-wise description)

    let caretPositionRectangle: CGRect = textView.caretRect(for: cursorPosition)
    // now use either the whole rectangle, or its origin (caretPositionRectangle.origin)
}

textView.selectedTextRange?.start returns a text position of the cursor, and we then simply use textView.caretRect(for:) to get its pixel position in textView.

Milan Nosáľ
  • 19,169
  • 4
  • 55
  • 90
10

It's painful, but you can use the UIStringDrawing additions to NSString to do it. Here's the general algorithm I used:

CGPoint origin = textView.frame.origin;
NSString* head = [textView.text substringToIndex:textView.selectedRange.location];
CGSize initialSize = [head sizeWithFont:textView.font constrainedToSize:textView.contentSize];
NSUInteger startOfLine = [head length];
while (startOfLine > 0) {
    /*
     * 1. Adjust startOfLine to the beginning of the first word before startOfLine
     * 2. Check if drawing the substring of head up to startOfLine causes a reduction in height compared to initialSize.
     * 3. If so, then you've identified the start of the line containing the cursor, otherwise keep going.
     */
}
NSString* tail = [head substringFromIndex:startOfLine];
CGSize lineSize = [tail sizeWithFont:textView.font forWidth:textView.contentSize.width lineBreakMode:UILineBreakModeWordWrap];
CGPoint cursor = origin;
cursor.x += lineSize.width;
cursor.y += initialSize.height - lineSize.height;
return cursor;
}

I used [NSCharacterSet whitespaceAndNewlineCharacterSet] to find word boundaries.

This can also be done (presumably more efficiently) using CTFrameSetter in CoreText, but that is not available in iPhone OS 3.1.3, so if you're targeting the iPhone you will need to stick to UIStringDrawing.

Akshay Sunderwani
  • 12,428
  • 8
  • 29
  • 52
Tony
  • 3,470
  • 1
  • 18
  • 23
  • 1
    It's a nice workaround I also played around with. To find word boundaries I found it best to use CFStringTokenizer. Your implementation doesn't give the x and y coordinates but rather the size of the chars in the line containing the caret until the caret, right? However, I accept your approach as an answer since I eventually did something similar adopted to my needs. – Raphael Schaad May 12 '10 at 17:25
  • Thanks for the pointer about CFStringTokenizer, I overlooked that. Subtracting lineSize.height from initialSize.height will give you the y offset of the cursor (relative to the UITextView) and the width of the final line fragment gives you the x offset. – Tony May 17 '10 at 17:15
  • Can you elaborate more on /* * 1. Adjust startOfLine to the beginning of the first word before startOfLine * 2. Check if drawing the substring of head up to startOfLine causes a reduction in height compared to initialSize. * 3. If so, then you've identified the start of the line containing the cursor, otherwise keep going. */ I also require cursor position in UITextView. – tek3 Jun 15 '10 at 13:18
  • 1
    @tek3: initialSize is the total rectangle containing the whole string, but it doesn't tell you what x-coordinate the cursor is at in the last line. By chopping off words from the end and sizing the incrementally smaller string, you can find the index for the beginning of the last line. Then, using that, you can find the width of the last line up to the cursor (lineSize). (By the way, this is a really nice answer.) – Daniel Dickison Oct 12 '10 at 18:57
  • I think, to be honest *tek3* just wants to know what code to put inside the `while` function. – Joshua Oct 13 '10 at 15:22
  • @tony, i have implement ur algorithm, but when the keyboard appear in the 2nd time or higher, selectedRange.location always show 2147483647, it means that the location is not found. So, how can i found it location? in where method i have to implement or called this algorithm exatcly?? really need ur answer please... – R. Dewi Jan 06 '11 at 10:20
  • @tony see my question, please http://stackoverflow.com/questions/2633379/pixel-position-of-cursor-in-uitextview – R. Dewi Jan 07 '11 at 04:08
3

Yes — as in there's a method to get the cursor position. Just use

CGRect caretRect = [textView rectContainingCaretSelection];
return caretRect.origin;

No — as in this method is private. There's no public API for this.

kennytm
  • 510,854
  • 105
  • 1,084
  • 1,005
1

I try to mark a selected text, i.e. I receive a NSRange and want to draw a yellow rectangle behind that text. Is there another way?

I can advise you some trick:

NSRange selectedRange = myTextView.selectedRange;
[myTextView select:self];
UIMenuController* sharedMenu = [UIMenuController sharedMenuController];
CGRect menuFrame = [sharedMenu menuFrame];
[sharedMenu setMenuVisible:NO];
myTextView.selectedRange = selectedRange

Using this code, you can know get the position of the cut/copy/past menu and there place your yellow rectangle.

I did not find a way to get the menu position witout forcing it to appear by a simulated select operation.

Regards Assayag

Epaga
  • 38,231
  • 58
  • 157
  • 245
Mayosse
  • 696
  • 1
  • 7
  • 16
  • I implemented this for testing and it's a nice hack but actually not useful since you can't conclude from menuFrame to the cursor position (problems when arrow is pointing down and not up etc.). – Raphael Schaad Sep 07 '10 at 18:42
1

Take a screenshot of the UITextView, then search the pixel data for colors that match the color of the cursor.

-(CGPoint)positionOfCursorForTextView:(UITextView)textView {
     //get CGImage from textView
     UIGraphicsBeginImageContext(textView.bounds.size);
     [textView.layer renderInContext:UIGraphicsGetCurrentContext()];
     CGImageRef textImageRef = UIGraphicsGetImageFromCurrentImageContext().CGImage;
     UIGraphicsEndImageContext();  

     //get raw pixel data
     CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
     uint8_t * textBuffer = (uint8_t*)malloc(Width * Height * 4);
     NSUInteger bytesPerRow = 4 * Width;
     NSUInteger bitsPerComponent = 8;
     CGContextRef context = CGBitmapContextCreate(textBuffer, Width, Height,
                                             bitsPerComponent, bytesPerRow, colorSpace,
                                             kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);


     CGColorSpaceRelease(colorSpace);

     CGContextDrawImage(context, CGRectMake(0, 0, Width, Height), textImageRef);
     CGContextRelease(context);

     //search 

     for(int y = 0; y < Height; y++)
     {
         for(int x = 0; x < Width * 4; x += 4)
         {
             int red   = textBuffer[y * 4 * (NSInteger)Width + x];
             int green = textBuffer[y * 4 * (NSInteger)Width + x + 1];    
             int blue  = textBuffer[y * 4 * (NSInteger)Width + x + 2];  
             int alpha   = textBuffer[y * 4 * (NSInteger)Width + x + 3];    


             if(COLOR IS CLOSE TO COLOR OF CURSOR)
             {
                 free(textBuffer);
                 CGImageRelease(textImageRef);
                 return CGPointMake(x/4, y);
             }
         }
     }

     free(textBuffer);
     CGImageRelease(textImageRef);
     return CGPointZero;
}
Akshay Sunderwani
  • 12,428
  • 8
  • 29
  • 52
cncool
  • 45
  • 1