10

I want to be able to replace some text in an UITextView programatically, so I wrote this method as an UITextView category:

- (void) replaceCharactersInRange:(NSRange)range withString:(NSString *)newText{

    self.scrollEnabled = NO;

    NSMutableString *textStorage = [self.text mutableCopy];
    [textStorage replaceCharactersInRange:range withString:newText];

    //replace text but undo manager is not working well
    [[self.undoManager prepareWithInvocationTarget:self] replaceCharactersInRange:NSMakeRange(range.location, newText.length) 
                                                                       withString:[textStorage substringWithRange:range]];
    NSLog(@"before replacing: canUndo:%d", [self.undoManager canUndo]); //prints YES
    self.text = textStorage; 
    NSLog(@"after replacing: canUndo:%d", [self.undoManager canUndo]); //prints NO
    if (![self.undoManager isUndoing])[self.undoManager setActionName:@"replace characters"];
    [textStorage release];

    //new range:
    range.location = range.location + newText.length;
    range.length = 0;
    self.selectedRange = range;

    self.scrollEnabled = YES;

}

It works but NSUndoManager stops working (it seems to be reset) just after doing self.text=textStorage I have found a private API: -insertText:(NSString *) that can do the job but who knows if Apple is going to approve my app if I use it. Is there any way to get text replaced in UITextView with NSUndoManager Support? Or maybe I am missing something here?

jv42
  • 8,521
  • 5
  • 40
  • 64
nacho4d
  • 43,720
  • 45
  • 157
  • 240

4 Answers4

7

There is actually no reason for any hacks or custom categories to accomplish this. You can use the built in UITextInput Protocol method replaceRange:withText:. For inserting text you can simply do:

[textView replaceRange:textView.selectedTextRange withText:replacementText];

This works as of iOS 5.0. Undo works automatically and there are no weird scrolling issues.

mightylost
  • 86
  • 1
  • 2
  • 1
    This is not 100% true. `replaceRange:withText` will do the job and UITextInput protocol is indeed available from 3.2 but UITextView didn't adopt it until 5.0. So this answer only will work on OS5.0 and above – nacho4d Apr 27 '12 at 04:09
  • I updated my answer based on @nacho4d 's comment. Good catch. – mightylost Jul 09 '12 at 22:42
  • Yeah, that's the iOS 5 way to do it. – Max Seelemann Sep 20 '12 at 14:13
  • I've changed my answer since we are in iOS9 already. Also because I found that using the clipboard-paste approach can be very inefficient in cases where the clipboard contains big data like images – nacho4d Nov 21 '15 at 12:41
5

After having stumbled over this issue and getting grey hair of it, I finally found a satisfying solution. It's a bit tacky, but it DOES work like a charm. The idea is to use the working copy&paste support that UITextView offers! I think you might be interested:

- (void)insertText:(NSString *)insert
{
    UIPasteboard *pboard = [UIPasteboard generalPasteboard];

    // Clear the current pasteboard
    NSArray *oldItems = [pboard.items copy];
    pboard.items = nil;

    // Set the new content to copy
    pboard.string = insert;

    // Paste
    [self paste: nil];

    // Restore pasteboard
    pboard.items = oldItems;
    [oldItems release];
}

EDIT: Of course you can customize this code to insert text at any position in the text view. Just set the selectedRange before calling paste.

jv42
  • 8,521
  • 5
  • 40
  • 64
Max Seelemann
  • 9,344
  • 4
  • 34
  • 40
  • The only demerit of this method is that the action name will be "Undo Paste" instead of "Undo Typing". (This message appears when shaking the device so the NSUndoManager alert will show up) – nacho4d Jul 09 '11 at 20:35
  • @nacho4d that's right. But at least on iPad most users will actually use the undo/redo button on the keyboard that does not show the name. And did you try to give it a custom name with `NSUndoManager`'s `setActionName:`? Maybe you need a undo group or so – Max Seelemann Jul 14 '11 at 12:22
  • How would I change "Undo Paste" to "Undo Typing"? Can I just call setActionName: after [self paste:nil] ? – nacho4d Jul 15 '11 at 04:26
  • 1
    I haven't tried, but If it can be changed this seems to be the only option. Maybe even add an undo grouping around the calls. – Max Seelemann Aug 08 '11 at 07:55
  • I did tried to change the action name but it didn't work. Apparently there is no perfect solution for this yet. Other than that this is OK :) – nacho4d Apr 12 '12 at 16:47
  • hi @MaxSeelemann i have a simillar problem here, but the problem is i wanna support an undo redo using iPad bluetooth keyboard, it's use cmd + z press button. How to use this answer to solver my problem? can you help me? – R. Dewi Sep 20 '12 at 11:59
  • @RDewi CMD-Z is using the same undo stack as the on-keyboard controls. There shouldn't be any difference... – Max Seelemann Sep 20 '12 at 14:11
  • In the end using this approach worked for me. However at that time I decided not to use it because when the clipboard contains big data like images (copied from the Photos.app for example) inserting text in this way is very very slow. See @mightlylost solution – nacho4d Nov 21 '15 at 12:43
1

Shouldn't it be

[[self.undoManager prepareWithInvocationTarget:self] replaceCharactersInRange:NSMakeRange(range.location, newText.length) 
                                                                   withString:[self.text substringWithRange:range]];

And you probably can remove the mutable string and just do,

self.text = [self.text stringByReplacingCharactersInRange:range withString:newText];
Deepak Danduprolu
  • 44,595
  • 12
  • 101
  • 105
  • I think you are right, it should be that way. I tried it but that is not solving the problem. the NSUndoManager can't undo as soon as `self.text = textStorage` is done. – nacho4d Jun 21 '11 at 03:20
  • When do you trigger this method? I would like to reproduce this. If you can give me some sample code, all the better. – Deepak Danduprolu Jun 21 '11 at 03:22
  • I have an inputAccessoryView for my UITexView, with some buttons. I use this when a button is pressed to insert some chars in the text – nacho4d Jun 21 '11 at 03:26
  • I have tried your code [`here`](http://dl.dropbox.com/u/22783696/Undodo.zip) and it seems to be working without hiccups. Can you tell me what I am doing different? – Deepak Danduprolu Jun 21 '11 at 03:46
  • Are you doing it while editing? It's weird... I wonder if there is a way to see the stack of invocations piled up in NSUndoManager so I can make sure everything is working as expected ... – nacho4d Jun 21 '11 at 04:10
  • Yes I am doing it while editing. Probably can check if you're doing it any other way. As such there is no public way to inspect the undo stack. If you just want to do it in development mode, you can check [`this`](http://parmanoir.com/Inspecting_NSUndoManager's_undo_stack). – Deepak Danduprolu Jun 21 '11 at 04:28
  • Deepak, I found that above method will successfully record the action into NSUndoManager but most actions before it are reset. Try inputing "Hello" from the keyboard then append "-" programatically. Yes, NSUndoManager can undo but the first undo will do nothing, the second will undo "-" and there is no third one. :( – nacho4d Jun 21 '11 at 05:54
0

I've been having trouble with this for a long time. I finally realized that you have to make sure you register the undo operation BEFORE doing any changes to your text storage.

diegoreymendez
  • 1,997
  • 18
  • 20