[localhost:~]$ cat * > /dev/ragfield

Tuesday, September 15, 2009

Insert text at the current cursor location in a UITextField on iPhone OS 3.0

The text widgets (UITextField, UITextView, etc) in the iPhone OS SDK appear to have been intentionally designed to allow as little control for 3rd party developers as possible. I found myself needing a user interface in an iPhone application which has buttons (in addition to the standard keyboard) that insert text into the currently selected UITextField. Neither UITextField nor any of it's parent classes or protocols have such a method.

One possible solution would be to get the contents of the UITextField, append the desired text to it, then set the contents of the UITextField to the new string. This gives the expected behavior if the text cursor is at the end of the text (which it usually is), but it does not give the correct behavior when the text cursor is anywhere else within the text.

There is a solution in iPhone OS 3.0. The method -(void)paste:(id)sender inserts the text from the system pasteboard at the current text cursor location. So all we need to do is temporarily hijack the system pasteboard. The basic steps are as follows:

  1. get a reference to the system pasteboard
  2. save the contents of the pasteboard so you can restore them later
  3. change the contents of the pasteboard to include the text you wish to insert
  4. send the -(void)paste:(id)sender message to the responder (UITextField or UITextView)
  5. restore the contents of the system pasteboard

Here is a simple category on UIResponder which adds a -(void)insertText:(NSString*)text method to this base class that should work on any text editing view.

@interface UIResponder(UIResponderInsertTextAdditions)
- (void) insertText: (NSString*) text;
@end

@implementation UIResponder(UIResponderInsertTextAdditions)

- (void) insertText: (NSString*) text
{
	// Get a refererence to the system pasteboard because that's
	// the only one @selector(paste:) will use.
	UIPasteboard* generalPasteboard = [UIPasteboard generalPasteboard];
	
	// Save a copy of the system pasteboard's items
	// so we can restore them later.
	NSArray* items = [generalPasteboard.items copy];
	
	// Set the contents of the system pasteboard
	// to the text we wish to insert.
	generalPasteboard.string = text;
	
	// Tell this responder to paste the contents of the
	// system pasteboard at the current cursor location.
	[self paste: self];
	
	// Restore the system pasteboard to its original items.
	generalPasteboard.items = items;
	
	// Free the items array we copied earlier.
	[items release];
}

@end

35 comments:

QTech said...

You are a lifesaver! Cheers!
I've been struggling with this issue for the last 2 days. I've tried so many different approaches but they all suffer from different and unacceptable side effects. Some have the textview bouncing around strangely and uncontrollably and one attempt was using the undocumented setContentToHTMLString which solved the bouncing issues but when the global \n to
replacements were made you could not do a carriage return at the end of the document - I guess HTML ignores it.

Thanks again

EzD said...

You just saved me a few hours. I actually didn't know there was a paste method on UIResponder. Thank you!

Dan said...

Great solution - thanks!

I've posted a link to this webpage in an answer on www.stackoverflow.com.

EzD said...

I just discovered that Apple has provided a hidden insertText method. I attempted to debug in 3.2 and my breakpoint never got hit. I removed the code, and although I had a warning that the method didn't exist, it worked correctly. It's almost as if Apple copied your code. ;)

Scott Lahteine said...

Nice! Found your tip through Stack Overflow.

I seriously needed to save people from having to type substitutes for ∆ ΓΈ ♭ ♯ and other musical symbols in my app "ChordCalc," and this was the last key I needed to make it work. The text even gets filtered through the shouldChangeCharactersInRange delegate method!

I combined your tip with the one about playing system sound 0x450 to make the key click, and my faux keyboard is crazy good. I even implemented the funky enlarged key flyout when you type and the little 0.075 second delay before hiding it. No one will know I cheated but Apple, heh-heh...

Thank you for your invaluable help!

Edward said...

I got "[UIResponder paste:]: unrecognized selector" when call [self paste: self]

Any clue? Thank you.

Ragfield said...

@edward, from UIResponder.h:

@interface NSObject(UIResponderStandardEditActions) // these methods are not implemented in NSObject

- (void)cut:(id)sender __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_3_0);
- (void)copy:(id)sender __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_3_0);
- (void)paste:(id)sender __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_3_0);
- (void)select:(id)sender __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_3_0);
- (void)selectAll:(id)sender __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_3_0);

@end

Are you building against the 3.0 SDK or an earlier SDK?

Edward said...

I am building my app against iPhone 3.0. What I want to do is to paste text into UIWebView without clicking "paste" menu.

I have an action:
-(IBAction) myPaste:(id)sender;

I want to call uiresponder paste from my own action.

I am still very new to iPhone dev. Your solution is what I am looking for. But I have not figure out the correct way to implement it. Thank you for your help in advance.

Ragfield said...

@edward, I don't think there's any public way to do that. This will work with UITextField and UITextView. UIWebView is a different beast. It does not implement the UITextInputTraits protocol and it contains many subviews.

If you were looking for a non-supported (non-app store) way to do this with a UIWebView you might try walking the UIWebView's subviews (and their subviews, etc) until you found a UITextField (or something else that implements the UITextInputTraits protocol.

Edward said...

Do I need to walk through all the subviews in UIWebView to find the UITextField? Is there any way to detect where the cursor is in the UIWebView? Thank you

Ragfield said...

@edward there is no supported way to do what you're trying to do. UIWebView allows for very little customization.

Edward said...

- (NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;

I am trying to figure out how to use the above function in UIWebView and call a javascript function to do this.

Edward said...

load a local javascript into the app with UIWebView. Then use the function of javascript to insert Text where the focus is. It works. :)

Thank you for your help.

Jail said...

Does any one tried to do this with UISearchBar? It looks like it doesn't implement paste: method, which is very strange.

Anonymous said...

-(IBAction) updateTextA:(id)sender {
respondertxt = [UIResponder new];

text = @"A";
[respondertxt insertText:(NSString *) @"A"];


}

What am I doing wrong ?? The app is crashing ..please help

Sim said...

As EzD said before insertText: is implemented in iPhoneOS 3.2 (UIKeyInput protocol). But there is no deleteBackward method.
Is it possible te delete char in cursor position?

Phillip said...

deleteBackward
Delete a character from the displayed text. (required)

- (void)deleteBackward

Discussion
Remove the character just before the cursor from your class’s backing store and redisplay the text.

Availability
Available in iPhone OS 3.2 and later.
Declared In
UITextInput.h

Anonymous said...

When I attempt to use deleteBackward on a UITextField object (or for that matter delete:sender), I always get error: "*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[UITextField deleteBackward]: unrecognized selector sent to instance 0x4c56590'".

The insertText:text and paste:sender methods work fine without custom code. I don't think Apple actually created default code for these methods, unless I'm doing something vastly wrong.

Sim said...

I used the below code to simulate Backspace key:
NSRange range = [activeField performNSRangeSelector:@selector(selectionRange)];
if (range.location == NSNotFound) return;
range.location--; range.length = 1;
[oldText replaceCharactersInRange:range withString:@""];
[activeField performSelector:@selector(setText:) withObject:oldText];
range.length = 0;
[activeField performSelector:selector withRange:range];

Sim said...

There are 2 methods for previous snip:
@implementation NSObject (my)

-(NSRange)performNSRangeSelector:(SEL)theSelector {
NSRange result = NSMakeRange(NSNotFound, 0);

NSInvocation *anInvocation = [NSInvocation invocationWithMethodSignature:[[self class] instanceMethodSignatureForSelector:theSelector]];
[anInvocation setSelector:theSelector];
[anInvocation setTarget:self];
[anInvocation invoke];
[anInvocation getReturnValue:&result];
return result;
}
-(void)performSelector:(SEL)theSelector withRange:(NSRange)range {
NSInvocation *anInvocation = [NSInvocation invocationWithMethodSignature:[[self class] instanceMethodSignatureForSelector:theSelector]];
[anInvocation setSelector:theSelector];
[anInvocation setTarget:self];
[anInvocation setArgument:&range atIndex:2];
[anInvocation invoke];
}
@end

doblezeta said...

Has anyone found a solution for deleting the character to the left of the cursor? The post of the guy above doesn't seem to make sense.

Adrian Falvey said...

Very nice implementation and works fine for me on iPad 3.2. Thanks for posting this tip.

David said...

This is spectacular. Thank you so much!

@Jail: I'm doing it with a UISearchBar, but I first grab the search bar's UITextField by walking the UISearchBar's subviews. I don't know if this would pass App Store muster or not, but it's not an issue for me.

Lasse Thomasson said...

Absolutly brilliant. Saved my day and tought me something new. Thanks!

Anonymous said...

Is there way to take care the `deleteBackward` by using `cut`? Now you might be thinking just replace the string with the new one. If you proceed and replace the UITextField.text with a new string. Your cursor position will be put to the end of the string.

Anonymous said...

Can anyone upload sample code which uses this approach?

Anonymous said...

Excellent post. Thanks for sharing!

peterb said...

This tip was posted a while ago but new people keep discovering it and it's terrific! But I haven't been able to use it to reproduce the "delete backward" function to just erase a character at end of or within a textfield, so my custom keyboard implementation remains incomplete. The posts by @Sim seem to be an attempt to reproduce that, but I don't follow his code. Anyone have any suggestions?

bloodthirstier said...

You genius! Thank you!

Somebody Nobody Sent said...

Brilliant!!!

Somebody Nobody Sent said...

And thank you.

Francois Robert said...

Very good trick. I used it for almost a year.
The only problem is that when the pasteboard contains a large item like an image, there a few seconds delay and sometimes iOS will kill the app as non-responsive.

Sri said...
This comment has been removed by the author.
Sri said...

How to automate the insertion of a text at the cursor location. I want to use in in my automation code. The code which performs click on the text field and send the text at the cursor position. this code should run from the testing code not on the original source code.

Matt said...

For those still looking to backspace in the middle of the input text. I found the answer here [SO].

Here's the code I'm using.
[self.textField replaceRange:self.textField.selectedTextRange withText:@""];

@"" will perform a backspace and @" " will do a space.