#swift #cocoa #objective-c-blocks #nsundomanager
Вопрос:
У меня проблемы с использованием UndoManager / NSUndoManager с асинхронной или длительной задачей. У меня есть решение, которое работает, но довольно сложное — намного больше, чем кажется разумным для довольно распространенной проблемы. Я опубликую это в качестве ответа и надеюсь на лучшие.
Проблема 1:
Моя невыполнимая задача не выполняется в текущем цикле выполнения. Такая задача может быть короткой операцией с обратным вызовом, который называется асинхронно. Это также может быть длительная операция, для которой я могу показать индикатор выполнения или даже предложить возможность отмены.
Проблема 2:
Моя невыполнимая задача может завершиться неудачей или быть отменена. Или, что еще хуже, задача повтора может провалиться. Пример: Я перемещаю файл, после отмены я обнаруживаю, что файл исчез из нового местоположения. Я не должен помещать задачу повтора обратно в стек.
Идея 1:
Я мог бы отменить/повторить регистрацию по завершении задачи. Нельзя отменить операцию, которая еще не завершена, была отменена или завершилась неудачно. При такой настройке я не могу правильно настроить операцию и ее отмену: повтор не работает. Пример: пользователь запрашивает файл для копирования. В конце операции копирования я регистрирую операцию в UndoManager. Пользователь выбирает отмену. Я снова жду завершения операции, чтобы зарегистрироваться в UndoManager. Теперь диспетчер удаления не знает, что только что завершенное удаление файла на самом деле является обратной операцией для предыдущей операции копирования. Вместо того, чтобы предлагать пользователю возможность повторить копию, он предлагает возможность отменить удаление
Идея 2:
Отключите автоматическую группировку отмены. Я не понимаю, как я мог бы сделать это с помощью длительной операции. Я хочу автоматическую группировку для большинства других задач.
Я не мог заставить это работать с помощью простой операции с обратным вызовом asnyc. Этот бросок: «Вызывается эндогруппировка без начала сопоставления»
let assets = PHAsset.fetchAssets(in: album, options: nil)
let parent = PHCollectionList.fetchCollectionListsContaining(album, options: nil).firstObject
if let undoManager = undoManager {
undoManager.groupsByEvent = false
undoManager.beginUndoGrouping()
let isUndoManagerOperation = undoManager.isUndoing || undoManager.isRedoing
let targetSelf = Controller.self as AnyObject
undoManager.registerUndo(withTarget: targetSelf) { [weak undoManager] targetSelf in
Controller.createAlbum(for: assets, title: album.localizedTitle, parent: parent, with: undoManager, completionHandler: nil)
}
if !isUndoManagerOperation {
undoManager.setActionName(NSLocalizedString("Delete Album", comment: "Undoable action: Delete Album"))
}
}
PHPhotoLibrary.shared().performChanges {
PHAssetCollectionChangeRequest.deleteAssetCollections(NSArray.init(object: album))
} completionHandler: { (success, error) in
DispatchQueue.main.async {
undoManager?.endUndoGrouping()
undoManager?.groupsByEvent = true
}
}
Ответ №1:
Это сложное решение. Это работает, но в лучшем случае это инновационный хак.
Основы:
Я регистрируюсь в NSUndoManager только после завершения длительной задачи. Проблема, которая тогда возникает, заключается в том, что симметричная операция отмены также является длительной задачей и также регистрируется после завершения. NSUndoManager видит две отдельные операции, а не пару (повторное)выполнение/отмена.
Взлом 1:
В начале операции (начальная операция или операция отмены) я проверяю, выполняется ли в данный момент отмена или повторное выполнение. Затем он ожидает, что будет зарегистрирована обратная операция. Он ожидает, что текущая операция отмены будет сопряжена/сбалансирована с операцией повтора. Я даю ему фиктивную операцию:
if (undoManager.undoing || undoManager.redoing) {
NSObject *dummy = [[NSObject alloc] init];
[undoManager registerUndoWithTarget:dummy selector:@selector(description) object:nil];
[undoManager performSelector:@selector(removeAllActionsWithTarget:)
withObject:dummy
afterDelay:0.0];
}
Затем я удаляю эту операцию из стека отмены. Стек отмены теперь находится в разумном / согласованном состоянии. Однако я потерял возможность повторить операцию, которую я в настоящее время отменяю.
Взлом 2:
Когда задача отмены завершается, я не могу просто зарегистрироваться в диспетчере отмен: это уже было сделано (и очищено) при запуске задачи. Вместо этого я регистрирую задачу, которая ничего не делает, но снова регистрируется в диспетчере отмен. Затем позвольте менеджеру отмены отменить. Идея: я подделываю выполнение исходной операции, чтобы, когда она будет отменена, я мог зарегистрировать операцию повтора.
if (self.undoing) {
[[undoManager prepareWithInvocationTarget:[self class]] dummyTaskWithArguments:arguments];
[undoManager undo];
}
else {
[[undoManager prepareWithInvocationTarget:[self class]] taskWithArguments:arguments];
}
(void)dummyTaskWithArguments:(id)arguments
{
[[undoManager prepareWithInvocationTarget:[self class]] taskWithArguments:arguments];
}