简体   繁体   中英

How can I insert text into UITextField at the current cursor position?

I'm trying to use the UITextField's "return" key to insert a custom character. Here's what my UITextFieldDelegate method looks like:

- (BOOL)textFieldShouldReturn:(UITextField *)textField {
    [textField insertText:@"¶"];
    return NO;
}

Unfortunately, this only works some of the time:

  • "one two|" --> move cursor --> "one| two" --> return --> "one¶| two" ( OK )
  • "onetwo|" --> return --> "onetwo¶|" ( OK )
  • "onetwo|" --> move cursor --> "one|two" --> return --> "onetwo¶|" ( FAIL )

In the last case I would have expected "one¶|two".

How do I ensure that the inserted text is always inserted at the cursor position?

Thanks.

The problem is when you tap the return key on the keyboard, the text field sets the selected range (the cursor position) to the end of its text before it sends you the textFieldShouldReturn: message.

You need to keep track of the cursor position so you can restore it to its prior position. Let's say you have a reference to the text field in a property:

@interface ViewController () <UITextFieldDelegate>

@property (strong, nonatomic) IBOutlet UITextField *textField;

@end

You'll need an instance variable to hold the prior selected text range (from before the return key was tapped):

@implementation ViewController {
    UITextRange *priorSelectedTextRange_;
}

Then you can write a method that saves the selected text range to the instance variable:

- (void)saveTextFieldSelectedTextRange {
    priorSelectedTextRange_ = self.textField.selectedTextRange;
}

and in textFieldShouldReturn: , before you insert the pilcrow, you can change the selected text range back to its prior value:

- (BOOL)textFieldShouldReturn:(UITextField *)textField {
    textField.selectedTextRange = priorSelectedTextRange_;
    [textField insertText:@"¶"];
    return NO;
}

But how can we make the system send the saveTextFieldSelectedTextRange message when we need it to?

  • The UITextFieldDelegate protocol doesn't have messages for changes to the selected range.

  • UITextField doesn't post any notifications for changes to the selected range.

  • The UITextInputDelegate protocol does have selectionWillChange: and selectionDidChange: messages, but the system sets the text field's inputDelegate to its own UIKeyboardImpl object when the text field begins editing, so we can't use the inputDelegate .

  • Key-value observing on the text field's selectedTextRange property isn't reliable. In my testing on the iOS 6.0 simulator, I don't get a KVO message when I move the cursor from the middle to the end of the text by tapping the text field.

The only way I can think of to reliably track changes to the text field's selected range is by adding an observer to the run loop. On every pass through the event loop, the observer runs before event processing, so it can grab the current selected range before it changes.

So we actually need another instance variable, to hold the reference to our run loop observer:

@implementation ViewController {
    UITextRange *priorSelectedTextRange_;
    CFRunLoopObserverRef runLoopObserver_;
}

We create the observer in viewDidLoad :

- (void)viewDidLoad {
    [super viewDidLoad];
    [self createRunLoopObserver];
}

and we destroy it in both viewDidUnload and in dealloc :

- (void)viewDidUnload {
    [super viewDidUnload];
    [self destroyRunLoopObserver];
}

- (void)dealloc {
    [self destroyRunLoopObserver];
}

To create the observer, we need a plain old C function for it to call. Here's that function:

static void runLoopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    __unsafe_unretained ViewController *self = (__bridge ViewController *)info;
    [self saveTextFieldSelectedTextRange];
}

Now we can actually create the observer and register it with the main run loop:

- (void)createRunLoopObserver {
    runLoopObserver_ = CFRunLoopObserverCreate(NULL, kCFRunLoopAfterWaiting, YES, 0, &runLoopObserverCallback, &(CFRunLoopObserverContext){
        .version = 0,
        .info = (__bridge void *)self,
        .retain = CFRetain,
        .release = CFRelease,
        .copyDescription = CFCopyDescription
    });
    CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver_, kCFRunLoopCommonModes);
}

and here's how we actually deregister the observer and destroy it:

- (void)destroyRunLoopObserver {
    if (runLoopObserver_) {
        CFRunLoopRemoveObserver(CFRunLoopGetMain(), runLoopObserver_, kCFRunLoopCommonModes);
        CFRelease(runLoopObserver_);
        runLoopObserver_ = NULL;
    }
}

This approach works in my testing on the iOS 6.0 simulator.

What's happening here is that you're not keeping track of the insertion point, also known as the selection range.

And to do that, you need to get somewhat deeper into the guts of what UITextField can do.

Using UITextInput (accessible as a protocol that UITextField uses) , you can fetch the " selectedTextRange " property which tells you where the caret (cursor, insertion point) is and that's where you should insert your special character. This should work if you set your object to be a delegate that conforms to the " UITextInput " protocol.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM