如何在UITextView中替换文本并支持NSUndoManager?

10

我希望能够通过编程方式替换UITextView中的一些文本,因此我编写了此方法作为UITextView类别:

- (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;

}

这个方法可以工作,但是在执行self.text=textStorage之后,NSUndoManager停止工作(似乎被重置了)。我找到了一个私有的API:-insertText:(NSString *),它可以完成这项工作,但谁知道如果我使用它,苹果是否会批准我的应用程序。有没有什么办法在具有NSUndoManager支持的UITextView中替换文本?或者我可能错过了什么吗?

4个回答

7
实际上,没有任何原因需要使用任何黑客技巧或自定义类别来完成此操作。您可以使用内置的UITextInput协议方法replaceRange:withText:。对于插入文本,您只需执行以下操作:
[textView replaceRange:textView.selectedTextRange withText:replacementText];

这个功能在iOS 5.0及以上版本中可用。撤销操作会自动进行,而且不会出现奇怪的滚动问题。


1
这并不是100%正确的。replaceRange:withText可以完成任务,UITextInput协议确实从3.2开始就可用了,但UITextView直到5.0才采用它。因此,这个答案只适用于OS5.0及以上版本。 - nacho4d
我根据@nacho4d的评论更新了我的答案。很好的发现。 - mightylost
是的,这就是iOS 5的做法。 - Max Seelemann
我改变了我的答案,因为我们已经在iOS9上了。同时,我发现使用剪贴板粘贴方法在剪贴板包含大量数据(如图像)的情况下可能非常低效。 - nacho4d

5

在遇到这个问题并且为此变得苍老之后,我终于找到了一个令人满意的解决方案。虽然有点俗气,但确实非常有效。这个想法是利用UITextView提供的工作副本和粘贴支持!我认为你可能会感兴趣:

- (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];
}

编辑:当然,您可以自定义此代码以在文本视图中的任何位置插入文本。只需在调用paste之前设置selectedRange即可。


这种方法唯一的缺点是操作名称将会是“撤销粘贴”而不是“撤销输入”(当晃动设备时,NSUndoManager警报将会出现)。 - nacho4d
@nacho4d 没错。但至少在iPad上,大多数用户实际上会使用键盘上的撤销/重做按钮,该按钮不显示名称。您尝试使用NSUndoManagersetActionName:自定义名称了吗?也许您需要一个撤销组或类似的东西。 - Max Seelemann
我该如何将“撤消粘贴”更改为“撤消输入”?我是否可以在[self paste:nil]之后直接调用setActionName:? - nacho4d
1
我还没有尝试过,但如果可以改变的话,这似乎是唯一的选择。甚至可以在调用周围添加撤销分组。 - Max Seelemann
最终,这种方法对我起作用了。但是在那个时候,我决定不使用它,因为当剪贴板包含大数据(例如从Photos.app复制的图像)时,用这种方式插入文本非常非常慢。可以看看@mightlylost的解决方案。 - nacho4d
显示剩余3条评论

1

它不应该是这样的吗?

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

而且你可能可以删除可变字符串,然后只需执行以下操作:

self.text = [self.text stringByReplacingCharactersInRange:range withString:newText];

你是在什么时候触发这个方法的?我想要复现这个过程。如果你能给我一些示例代码,那就更好了。 - Deepak Danduprolu
我为我的UITextView创建了一个inputAccessoryView,并添加了一些按钮。当按下按钮时,我使用它来插入一些字符到文本中。 - nacho4d
我已经尝试了你的代码here,看起来没有任何问题。你能告诉我我做了什么不同吗? - Deepak Danduprolu
是的,我正在编辑时进行操作。如果您使用其他方式进行操作,可能可以检查一下。目前没有公共方法来检查撤销堆栈。如果您只想在开发模式下进行操作,可以查看this - Deepak Danduprolu
Deepak,我发现上述方法可以成功地将操作记录到NSUndoManager中,但是在它之前的大多数操作都被重置了。尝试从键盘输入“Hello”,然后通过程序附加“-”。是的,NSUndoManager可以撤消,但第一次撤消不会有任何作用,第二次将撤消“-”,而第三次则没有了。 :( - nacho4d
显示剩余2条评论

0

我在这个问题上困扰了很长时间。最后我终于意识到,在对文本存储做任何更改之前,必须确保注册撤消操作。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接