带有异步或长时间运行任务的 UndoManager
Posted
技术标签:
【中文标题】带有异步或长时间运行任务的 UndoManager【英文标题】:UndoManager with async or long-running tasks 【发布时间】:2022-01-03 01:54:37 【问题描述】:我在将 UndoManager / NSUndoManager 用于异步或长时间运行的任务时遇到问题。我有一个可行的解决方案,但非常复杂——对于一个相当常见的问题来说,这远远超过了合理的解决方案。我会将其发布为答案,并希望有更好的答案。
问题一:
我的可撤消任务未在当前运行循环中完成。这样的任务可以是一个带有异步调用回调的简短操作。它也可以是一个长时间运行的操作,我可能会显示一个进度指示器,甚至提供取消选项。
问题 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
【问题讨论】:
【参考方案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];
【讨论】:
以上是关于带有异步或长时间运行任务的 UndoManager的主要内容,如果未能解决你的问题,请参考以下文章
在后台运行长时间运行的并行任务,同时允许小型异步任务更新前台
python的sched模块可以在任务执行期间不阻塞地运行异步任务吗?