Как я могу заменить текст в UITextView с поддержкой NSUndoManager?

Я хочу иметь возможность программно заменить некоторый текст в 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;

}

Это работает, но NSUndoManager перестает работать (кажется, что он сбрасывается) сразу после выполнения self.text=textStorage. Я нашел частный API: -insertText:(NSString *), который может выполнять эту работу, но кто знает, собирается ли Apple одобрить мое приложение, если я его использую. Есть ли способ заменить текст в UITextView с поддержкой NSUndoManager? Или, может быть, я что-то упускаю здесь?


person nacho4d    schedule 21.06.2011    source источник


Ответы (4)


На самом деле нет причин для каких-либо хаков или пользовательских категорий для достижения этого. Вы можете использовать встроенный метод протокола UITextInput replaceRange:withText:. Для вставки текста вы можете просто сделать:

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

Это работает с iOS 5.0. Отмена работает автоматически, и нет никаких странных проблем с прокруткой.

person mightylost    schedule 26.04.2012
comment
Это не на 100% правда. replaceRange:withText выполнит эту работу, и протокол UITextInput действительно доступен с версии 3.2, но UITextView не принимал его до версии 5.0. Так что этот ответ будет работать только на OS5.0 и выше. - person nacho4d; 27.04.2012
comment
Я обновил свой ответ на основе комментария @nacho4d. Хороший улов. - person mightylost; 10.07.2012
comment
Да, это способ iOS 5 сделать это. - person Max Seelemann; 20.09.2012
comment
Я изменил свой ответ, так как мы уже в iOS9. Кроме того, я обнаружил, что использование подхода с вставкой из буфера обмена может быть очень неэффективным в случаях, когда буфер обмена содержит большие данные, такие как изображения. - person nacho4d; 21.11.2015

После того, как я наткнулся на эту проблему и получил от нее седые волосы, я наконец нашел удовлетворительное решение. Это немного липко, но это ДЕЙСТВИТЕЛЬНО работает как шарм. Идея состоит в том, чтобы использовать рабочую поддержку копирования и вставки, которую предлагает 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];
}

EDIT: Конечно, вы можете настроить этот код для вставки текста в любое место в текстовом представлении. Просто установите selectedRange перед вызовом paste.

person Max Seelemann    schedule 08.07.2011
comment
Единственным недостатком этого метода является то, что имя действия будет Undo Paste вместо Undo Typing. (Это сообщение появляется при встряхивании устройства, поэтому появится предупреждение NSUndoManager) - person nacho4d; 10.07.2011
comment
@nacho4d верно. Но, по крайней мере, на iPad большинство пользователей на самом деле будут использовать кнопку отмены/возврата на клавиатуре, которая не показывает имя. И вы пытались дать ему собственное имя с setActionName: NSUndoManager? Может быть, вам нужна группа отмены или около того - person Max Seelemann; 14.07.2011
comment
Как бы я изменил Undo Paste на Undo Typing? Могу ли я просто вызвать setActionName: после [self paste:nil]? - person nacho4d; 15.07.2011
comment
Я не пробовал, но если это можно изменить, это, кажется, единственный вариант. Может быть, даже добавить группировку отмены вокруг вызовов. - person Max Seelemann; 08.08.2011
comment
Я пытался изменить имя действия, но это не сработало. Видимо, идеального решения для этого пока нет. В остальном это нормально :) - person nacho4d; 12.04.2012
comment
привет @MaxSeelemann У меня есть аналогичная проблема, но проблема в том, что я хочу поддерживать отмену повтора с помощью Bluetooth-клавиатуры iPad, для этого нужно нажать кнопку cmd + z. Как использовать этот ответ для решения моей проблемы? можешь мне помочь? - person R. Dewi; 20.09.2012
comment
@RDewi CMD-Z использует тот же стек отмены, что и элементы управления на клавиатуре. Разницы быть не должно... - person Max Seelemann; 20.09.2012
comment
В конце концов, использование этого подхода сработало для меня. Однако в то время я решил не использовать его, потому что, когда буфер обмена содержит большие данные, такие как изображения (например, скопированные из Photos.app), вставка текста таким образом очень и очень медленная. См. решение @mightlylost - person nacho4d; 21.11.2015

Разве это не должно быть

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

И вы, вероятно, можете удалить изменяемую строку и просто сделать,

self.text = [self.text stringByReplacingCharactersInRange:range withString:newText];
person Deepak Danduprolu    schedule 21.06.2011
comment
Я думаю, вы правы, так и должно быть. Я пробовал, но это не решает проблему. NSUndoManager не может отменить, как только будет выполнено self.text = textStorage. - person nacho4d; 21.06.2011
comment
Когда вы активируете этот метод? Я хотел бы воспроизвести это. Если вы можете дать мне пример кода, тем лучше. - person Deepak Danduprolu; 21.06.2011
comment
У меня есть inputAccessoryView для моего UITexView с некоторыми кнопками. Я использую это, когда нажимается кнопка, чтобы вставить некоторые символы в текст. - person nacho4d; 21.06.2011
comment
Я попробовал ваш код here, и он работает без сбоев. Можете ли вы сказать мне, что я делаю по-другому? - person Deepak Danduprolu; 21.06.2011
comment
Вы делаете это во время редактирования? Это странно... Интересно, есть ли способ увидеть стек вызовов, накопленный в NSUndoManager, чтобы я мог убедиться, что все работает как положено... - person nacho4d; 21.06.2011
comment
Да, я делаю это во время редактирования. Возможно, можно проверить, делаете ли вы это каким-либо другим способом. Таким образом, нет общедоступного способа проверить стек отмены. Если вы просто хотите сделать это в режиме разработки, вы можете проверить this. - person Deepak Danduprolu; 21.06.2011
comment
Deepak, я обнаружил, что описанный выше метод успешно записывает действие в NSUndoManager, но большинство действий перед ним сбрасываются. Попробуйте ввести Hello с клавиатуры, а затем добавить программно. Да, NSUndoManager может отменить, но первая отмена ничего не сделает, вторая отменит, а третьего нет. :( - person nacho4d; 21.06.2011

У меня уже давно с этим проблемы. Я наконец понял, что вы должны убедиться, что вы зарегистрировали операцию отмены, прежде чем вносить какие-либо изменения в ваше хранилище текста.

person diegoreymendez    schedule 20.11.2018