How can I replace text in UITextView with NSUndoManager support?
Asked Answered
P

4

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?

Perkins answered 21/6, 2011 at 2:49 Comment(0)
C
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.

Crabbing answered 26/4, 2012 at 18:26 Comment(4)
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 abovePerkins
I updated my answer based on @Perkins 's comment. Good catch.Crabbing
Yeah, that's the iOS 5 way to do it.Odont
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 imagesPerkins
O
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.

Odont answered 8/7, 2011 at 9:57 Comment(8)
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)Perkins
@Perkins 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 soOdont
How would I change "Undo Paste" to "Undo Typing"? Can I just call setActionName: after [self paste:nil] ?Perkins
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.Odont
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 :)Perkins
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?Neoma
@RDewi CMD-Z is using the same undo stack as the on-keyboard controls. There shouldn't be any difference...Odont
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 solutionPerkins
L
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];
Letendre answered 21/6, 2011 at 3:7 Comment(7)
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.Perkins
When do you trigger this method? I would like to reproduce this. If you can give me some sample code, all the better.Letendre
I have an inputAccessoryView for my UITexView, with some buttons. I use this when a button is pressed to insert some chars in the textPerkins
I have tried your code here and it seems to be working without hiccups. Can you tell me what I am doing different?Letendre
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 ...Perkins
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.Letendre
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. :(Perkins
W
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.

Warfore answered 20/11, 2018 at 16:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.