具有异步或长时间运行任务的UndoManager

问题描述 投票:0回答:2

我在使用 UndoManager / NSUndoManager 处理异步或长时间运行的任务时遇到问题。我有一个可行的解决方案,但相当复杂 - 远远超过了对于一个相当常见的问题来说似乎合理的解决方案。我会将其作为答案发布,并希望有更好的答案。

问题1:

我的可撤消任务未在当前运行循环中完成。这样的任务可以是一个简短的操作,带有异步调用的回调。它也可以是一个长时间运行的操作,我可能会显示进度指示器,甚至提供取消选项。

问题2:

我的可撤消任务可能会失败或被取消。或者更糟糕的是,重做任务可能会失败。示例:我移动了一个文件,撤消后我发现该文件已从新位置消失。我不应该将重做任务放回到堆栈上。

想法1:

我可以在任务完成时进行撤消/重做注册。无法撤消尚未完成、已取消或已失败的操作。使用此设置,我无法将操作及其撤消操作正确配对:重做不起作用。示例:用户请求复制文件。在复制操作结束时,我向 UndoManager 注册该操作。用户选择撤消。我再次等待操作完成,然后向 UndoManager 注册。现在UndoManager并不知道刚刚完成的文件删除实际上是之前复制操作的逆操作。它不是为用户提供重做复制的选项,而是提供撤消删除的选项

想法2:

禁用自动撤消分组。我不明白如何通过长时间运行的操作来做到这一点。我希望对大多数其他任务进行自动分组。

我无法通过带有 asnyc 回调的简单操作来实现它。此抛出:“调用 endUndoGrouping 时没有匹配的开始”

        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
            }
        }
swift cocoa objective-c-blocks nsundomanager
2个回答
1
投票

这是一个复杂的解决方案。它确实有效,但充其量只是一种创新的黑客技术。

基础知识:

我仅在长时间运行的任务完成后才向 NSUndoManager 注册。那么出现的问题是,对称撤消操作也是一个长时间运行的任务,并且也在完成后注册。 NSUndoManager 看到两个单独的操作,而不是(重新)执行/撤消对。

黑客1:

在操作开始时(初始操作或撤消操作),我检查 UndoManger 当前是否正在撤消或重做。然后它期望注册相反的操作。它期望当前的撤消操作与重做操作配对/平衡。我给它一个虚拟操作:

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

0
投票

由于 2021 年 11 月的解决方案不再有效,我想出了一个新的解决方案。

我现在在任务开始后立即向撤消管理器注册。同样,当操作被撤消时,我也立即向撤消管理器注册。

我创建了 HHUndoableTask 对象来跟踪操作的状态。后台任务可以检查操作是否已被取消。撤消操作可以检查任务是否已完成,从而准备撤消。

@implementation HHUndoableTask

NSString *const HHUndoableTaskRegisteredTasksKey =  @"HHUndoableTask.registeredTasks";

- (NSMutableArray *)registeredTasksForUndoManager:(NSUndoManager *)undoManager
{
    NSMutableArray *registeredTasks = [undoManager hh_associatedValueForKey:HHUndoableTaskRegisteredTasksKey];

    if (registeredTasks == nil) {
        registeredTasks = [NSMutableArray array];

        [undoManager hh_setAssociatedValue:registeredTasks forKey:HHUndoableTaskRegisteredTasksKey];
    }

    return registeredTasks;

}
- (void)registerWithUndoManager:(NSUndoManager *)undoManager handler:(void (^)(HHUndoableTask *undoableTask))handler
{
    NSMutableArray *registeredTasks = [self registeredTasksForUndoManager:undoManager];

    [undoManager registerUndoWithTarget:self handler:handler];

    [registeredTasks insertObject:self atIndex:0];

    NSInteger levelsOfUndo = (undoManager.levelsOfUndo != 0) ? undoManager.levelsOfUndo : 100;

    while ([registeredTasks count] > levelsOfUndo) {
        HHUndoableTask *last = [registeredTasks lastObject];

        [registeredTasks removeLastObject];
        [undoManager removeAllActionsWithTarget:last];
    }
}

- (void)removeFromUndoManager:(NSUndoManager *)undoManager
{
    NSMutableArray *registeredTasks = [self registeredTasksForUndoManager:undoManager];

    [undoManager removeAllActionsWithTarget:self];

    [registeredTasks removeObject:self];
}

@end

这用于启动长时间运行的操作的代码中:

- (void)start
{
    NSMutableDictionary *fileTagNames = [NSMutableDictionary dictionary];

    HHUndoableTask *undoableTask = [[HHUndoableTask alloc] init];

    undoableTask.data = fileTagNames;

    self.undoableTask = undoableTask;

    BOOL isUndoManagerOperation = undoManager.undoing || undoManager.isRedoing;

    if (!isUndoManagerOperation) {
        [undoManager setActionName:[NSString stringWithFormat:@"Tag %ld Files", [self.fileURLs count]]];
    }

    __weak NSUndoManager *weakUndoManager = undoManager;

    [undoableTask registerWithUndoManager:undoManager handler:^(HHUndoableTask *undoableTask) {
        NSDictionary *fileTagNames = undoableTask.data;
        NSUndoManager *undoManager = weakUndoManager;

        if ((![fileTagNames isKindOfClass:[NSDictionary class]]) || (undoManager == nil)) {
            return;
        }

        undoableTask.undone = true;

        if (!undoableTask.ready) {
            return;
        }

        // TODO: Call reverse operation
    }];

}

操作完成后:

- (void)stop
{
    HHUndoableTask  *undoableTask   = self.undoableTask;

    NSDictionary    *fileTagNames   = self.undoableTask.data;

    if ([fileTagNames count] > 0) {
        undoableTask.ready = !undoableTask.undone;
    }
    else {
        dispatch_async(dispatch_get_main_queue(), ^(void) {
            [undoableTask removeFromUndoManager:undoManager];
        });
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.