Cursor location in a uitextfield - iphone

im using customkeyboard in my controller. but how to get the cursor location in the uitextfiled. my requirement is to enter a charecter in a desired location of textfield. is it possible?

Let's assume that you code is in a method on an object where self.textField is the UITextField in question.
You can find the current position of the cursor/selection with:
NSRange range = self.textField.selectedTextRange;
If the user has not selected text the range.length will be 0, indicating that it is just a cursor. Unfortunately this property does not appear to be KVO compliant, so there is no efficient way to be informed when it changes, however this should not be a problem in your case because you probably really only care about it when you are responding to user interaction with your custom keyboard.
You can then use (assuming newText holds the input from your custom keyboard).
[self.textField replaceRange:range withText:newText];
If you need to subsequently adjust the cursor/selection you can use:
self.textField.selectedTextRange = newRange;
For example, you may want to position the cursor after the text you inserted.
UPDATE:
In my original answer I failed to notice that I was leveraging a category I had added to UITextView:
- (void)setSelectedRange:(NSRange)selectedRange
{
UITextPosition* from = [self positionFromPosition:self.beginningOfDocument offset:selectedRange.location];
UITextPosition* to = [self positionFromPosition:from offset:selectedRange.length];
self.selectedTextRange = [self textRangeFromPosition:from toPosition:to];
}
- (NSRange)selectedRange
{
UITextRange* range = self.selectedTextRange;
NSInteger location = [self offsetFromPosition:self.beginningOfDocument toPosition:range.start];
NSInteger length = [self offsetFromPosition:range.start toPosition:range.end];
NSAssert(location >= 0, #"Location is valid.");
NSAssert(length >= 0, #"Length is valid.");
return NSMakeRange(location, length);
}
Then replace use self.textField.selectedRange instead of self.textField.selectedTextRange and proceed as I described.
Thanks to omz for pointing out my error.
Of course, you can work directly with UITextRange but, at least in my case, this proved to be rather ungainly.

The answer is that you can't get the current cursor location for all types of editing that can be done with the textfield. You can insert characters at the cursor with [textField paste], but the user can move the cursor, select and modify text, without a way to get notified where the cursor ended up.
You can temporarily paste a special character and search its position in the string, remove it, and then add the character you want to have there.

Swift
Get the cursor location:
if let selectedRange = textField.selectedTextRange {
let cursorPosition = textField.offsetFromPosition(textField.beginningOfDocument, toPosition: selectedRange.start)
}
Enter text at some arbitrary location:
let arbitraryValue: Int = 5
if let newPosition = textField.positionFromPosition(textField.beginningOfDocument, inDirection: UITextLayoutDirection.Right, offset: arbitraryValue) {
textField.selectedTextRange = textField.textRangeFromPosition(newPosition, toPosition: newPosition)
textField.insertText("Hello")
}
My full answer is here.

Related

Detecting tapped character. UITextView characterRangeAtPoint always returns nil

I'm trying to determine, which particular character in UITextView has been tapped. I tried to use characterRangeAtPoint: method. But it always returns nil, wherever in the UITextView I tap.
I even wrote and run the following piece of code:
for (int x = 0; x<1000; x++) {
pos.x = x;
for (int y = 0; y<1000; y++) {
pos.y = y;
UITextRange * range = [textView characterRangeAtPoint:pos];
if (range!=nil) {
NSLog(#"x: %f, y: %f", pos.x, pos.y);
}
}
}
Well, it never reaches the NSLog string.
What am I doing wrong?
This is an iOS 7 bug. characterRangeAtPoint: always returns nil, unless some text has been selected at least once beforehand.
As a workaround, you could call setSelectedRange: or setSelectedTextRange: beforehand, but then the textView would highlight your selection. I came up with the following solution:
[textView select:textView];
[textView setSelectedTextRange:nil];
After you call these two methods, characterRangeAtPoint: will start working as expected.
Note that you just need to call this once after textView has been initialized.
EDIT: my answer was edited from "unless some text has been selected beforehand" to "unless some text is currently selected". This is wrong: text doesn't need to be currently selected, it just has to be selected once. All subsequent calls to setSelectedRange: will then be successful, as stated in my note. Edited back for further clarity.
Your UITextView must be selectable. In storyboard, check the "Selectable" checkbox.
In code,
textView.selectable = YES;
This is also required for the -closestPositionToPoint: method on UITextView.
Use shouldChangeCharactersInRange method instead of characterRangeAtPoint.
And use your If condition inplace of loop statement.
Is your TextView user-editable?
If YES, then try the texViewDidChangeSelection method. It is invoked every time the caret(cursor) is moved in a textView. Then get the cursor position by textView.selectedRange.location to point to the index, of the caret.
My assumption here is that you want the location of the character with respect to the number of character, and not co-ordinates in 2D.

Using an NSString as a token when editing UITextField

I have an NSArray of NSStrings which I want to use as tokens in a UITextField, such that I cannot select or delete individual characters in the tokens. Overall, I want to create an effect like those of calculators where pressing backspace would delete a whole of a function, not just individual characters from it. The token also has to be highlightable together with the other text.
How should I go about doing this?
(I'm trying to achieve the behaviour of the text field as seen in Calc 2M)
Edit:
I figured out a solution to this by making my text field use attributed strings, and I set a special colour for my tokens. I then intercept text selections using a gesture recogniser to fire an event to move the start and end points of the selection to wrap around the token(s) instead of the actual selection. I don't think this is the best way to do this so I'll leave this as an edit rather than an answer.
Edit 2:
Another question that aligns with mine: Problems with <UITextinputDelegate> protocol implementation
Under OS X this is done using a NSTokenField. There isn't an official implementation on iOS, but a few people rolled their own. Check this question for some examples: Is there an iPhone equivalent to the NSTokenField control?
Alright, after trying out a ton of methods to get around this, I decided to just use UITextView in place of UITextField as UITextFieldDelegate has the following method:
- (void)textViewDidChangeSelection:(UITextView *)textView
Since I wanted my tokens to stand out in my UITextView, I decided to use NSAttributedString to highlight my text. Since the tokens are now has attributes different form the normal text, I can use the following to check if the selected text is a token:
- (id)attribute:(NSString *)attributeName atIndex:(NSUInteger)index effectiveRange:(NSRangePointer)aRange
I wrote a method that will take in any UITextPosition for a given UITextView and return the nearest left start of a token in the form of a NSInteger. This is so that I can easily use it to make a NSRange afterwards:
- (NSInteger)textViewWrappedIntegerFromPosition:(UITextPosition *)position forTextField:(UITextView *)textView
{
NSInteger integer = [textView offsetFromPosition:textView.beginningOfDocument toPosition:position];
NSInteger newInteger = integer;
UIColor *color = [textView.attributedText attribute:NSForegroundColorAttributeName atIndex:integer-1 effectiveRange:NULL];
// left of it is a character from a token
if ([[UIColor colorWithRed:0.5 green:0.5 blue:1.0 alpha:1.0] isEqual:color])
{
NSInteger newInteger = integer+1;
UITextPosition *startMinus = [textView positionFromPosition:position offset:-1];
NSInteger startMinusInt = [textView offsetFromPosition:textView.beginningOfDocument toPosition:startMinus];
UIColor *colorMinus = [textView.attributedText attribute:NSForegroundColorAttributeName atIndex:startMinusInt effectiveRange:NULL];
// left and right of it is a character from a token
if ([[UIColor colorWithRed:0.5 green:0.5 blue:1.0 alpha:1.0] isEqual:colorMinus]) // both collision
{
while ([colorMinus isEqual:[UIColor colorWithRed:0.5 green:0.5 blue:1.0 alpha:1.0]] && startMinusInt > 0)
{
// I used offset -1 again because when I wrote this I was testing something else
position = [textView positionFromPosition:position offset:-1];
startMinusInt = [textView offsetFromPosition:textView.beginningOfDocument toPosition:position];
colorMinus = [textView.attributedText attribute:NSForegroundColorAttributeName atIndex:startMinusInt effectiveRange:NULL];
}
if (startMinusInt != 0)
{
newInteger = startMinusInt+1;
}
else
{
newInteger = startMinusInt;
}
return newInteger;
}
return newInteger;
}
return newInteger;
}
Using this method, I could compute if the start and end UITextPosition of a selection is in or adjacent to a token, and I can proceed to create a new range and set it as the new selection:
[textView setSelectedRange:NSRangeMake(newStartInt,newEndInt-newStartInt];

CoreText mapping characters

I have some in a touch handler which responds to a tap on a view that I've drawn some attributed text in. through this, I've got to the point where I have a CTRunRef (and the associated line) as well as the number of glyphs in that run.
What I'm not able to figure out easily, is how I can take that run of glyphs and, given my attributed string, map it out to characters in the string.
Specifically the problem is I would like to know what word the user tapped on in the view, so I can process whether or not that word is a URL and fire off a custom delegate method so I can open a web view with it. I have all the possible substrings, I just don't know how to map where the user tapped to a particular substring.
Any help would be greatly appreciated.
UPDATE: I've actually gone and done it a different way, on the suggestion of another person off of stackoverflow. Basically what I've done is to set a custom attribute, #"MyAppLinkAddress" with the value of the URL I found when I was converting the string to an attributed string. This happens before I draw the string. Therefore, when a tap event occurs, I just check if that attribute exists, and if so, call my delegate method, if not, just ignore it. It is working how I'd like now, but I'm going to leave this question open for a few more days, if someone can come up with an answer, I'll happily accept it if its a working solution so that some others may be able to find this information useful at some point in the future.
So as I mentioned in the update, I elected to go a different route. Instead I got the idea to use a custom attribute in the attributed string to specify my link, since I had it at creation time anyway. So I did that. Then in my touch handler, when a run is tapped, I check if that run has that attribute, and if so, call my delegate with it. From there I'm happily loading a webview with that URL.
EDIT: Below are snippets of code explaining what I did in this answer. Enjoy.
// When creating the attribute on your text store. Assumes you have the URL already.
// Filled in for convenience
NSRange urlRange = [tmpString rangeOfString:#"http://www.foo.com/"];
[self.textStore addAttribute:(NSString*)kCTForegroundColorAttributeName value:(id)[UIColor blueColor].CGColor range:urlRange];
[self.textStore addAttribute:#"CustomLinkAddress" value:urlString range:urlRange];
then...
// Touch handling code — Uses gesture recognizers, not old school touch handling.
// This is just a dump of code actually in use, read through it, ask questions if you
// don't understand it. I'll do my best to put it in context.
- (void)receivedTap:(UITapGestureRecognizer*)tapRecognizer
{
CGPoint point = [tapRecognizer locationInView:self];
if(CGRectContainsPoint(textRect, point))
{
CGContextRef context = UIGraphicsGetCurrentContext();
point.y = CGRectGetHeight(self.contentView.bounds) - kCellNameLabelHeight - point.y;
CFArrayRef lines = CTFrameGetLines(ctframe);
CFIndex lineCount = CFArrayGetCount(lines);
CGPoint origins[lineCount];
CTFrameGetLineOrigins(ctframe, CFRangeMake(0, 0), origins);
for(CFIndex idx = 0; idx < lineCount; idx++)
{
CTLineRef line = CFArrayGetValueAtIndex(lines, idx);
CGRect lineBounds = CTLineGetImageBounds(line, context);
lineBounds.origin.y += origins[idx].y;
if(CGRectContainsPoint(lineBounds, point))
{
CFArrayRef runs = CTLineGetGlyphRuns(line);
for(CFIndex j = 0; j < CFArrayGetCount(runs); j++)
{
CTRunRef run = CFArrayGetValueAtIndex(runs, j);
NSDictionary* attributes = (NSDictionary*)CTRunGetAttributes(run);
NSString* urlString = [attributes objectForKey:#"CustomLinkAddress"];
if(urlString && ![urlString isEqualToString:#""])
{
[self.delegate didReceiveURL:[NSURL URLWithString:urlString]];
UIGraphicsPopContext();
return;
}
}
}
}
UIGraphicsPopContext();
}
}
After you find the tapped line, you can ask for the index in string by calling CTLineGetStringIndexForPosition(). There's no need to access individual runs.

Can I Programmatically Select Text in UITextView?

I want to select Text on UITextView, similar to the default "Select" and "Select All" pop options we see when we tap. I want to the user the ability to do that from my custom menu. I played with selectedRange but that doesnt seem to do the trick. Any ideas?
Thanks
The selectedRange property should do it but, as mentioned in the documentation, only in iPhone OS 3.0 and later. In 2.2 and earlier, the selectedRange property is actually an insertion point.
As mentioned in the accepted answer, the selectedRange property is the thing you need, but beware that if you are using the -textViewDidBeginEditing: delegate method you might need to defer one run loop in order to win out over the user generated "insertion" action:
- (void)textViewDidBeginEditing:(UITextView *)textView
{
// Look for the default message and highlight it if present
NSRange defaultMsgRange = [textView.text rangeOfString:NSLocalizedString(#"TEXTFIELD_DEFAULT_MESSAGE", nil) options:NSAnchoredSearch];
BOOL isDefaultMsg = !(defaultMsgRange.location == NSNotFound && defaultMsgRange.length == 0);
if (isDefaultMsg) {
// Need to delay this by one run loop otherwise the insertion wins
[self performBlock:^(id sender) { // (BlocksKit - use GCD otherwise)
textView.selectedRange = defaultMsgRange;
} afterDelay:0.0];
}
}

Getting cursor position in a UITextView on the iPhone?

We have a UITextView in our iPhone app which is editable. We need to insert some text at the cursor location when the users presses some toolbar buttons but can't seem to find a documented (or undocumented) method of finding the current location of the cursor.
Does anybody have any ideas or has anybody else achieved anything similar?
Like drewh said, you can use UITextView's selectedRange to return the insertion point. The length of this range is always zero. The example below shows how to it.
NSString *contentsToAdd = #"some string";
NSRange cursorPosition = [tf selectedRange];
NSMutableString *tfContent = [[NSMutableString alloc] initWithString:[tf text]];
[tfContent insertString:contentsToAdd atIndex:cursorPosition.location];
[theTextField setText:tfContent];
[tfContent release];
Swift 4:
// lets be safe, thus if-let
if let cursorPosition = textView.selectedTextRange?.start {
// cursorPosition is a UITextPosition object describing position in the text
// if you want to know its position in textView in points:
let caretPositionRect = textView.caretRect(for: cursorPosition)
}
We simply use textView.selectedTextRange to get selected text range and cursor position is at its start position.
Use UITextView selectedRange property to find the insertion point when the text view is first responder. Otherwise, when the view is not in focus, this property returns NSNotFound. If you need to know the cursor position in that case, consider subclassing UITextView and overriding canResignFirstResponder method, where you can store cursor position to a member variable.
Have you tried UITextView.selectedRange? It returns an NSRange, whose location element should tell you, where the cursor is.