如何在Swift中注册NSUndoManager?

如何在Swift中使用NSUndoManager

这是我试图复制的一个Objective-C示例:

 [[undoManager prepareWithInvocationTarget:self] myArgumentlessMethod]; 

但是,Swift似乎没有NSInvocation ,这(看起来)意味着我不能在undoManager上调用它没有实现的方法。

我已经在Swift中尝试了基于对象的版本,但似乎让我的游乐场崩溃了:

 undoManager.registerUndoWithTarget(self, selector: Selector("myMethod"), object: nil) 

但是,它似乎崩溃,即使我的对象接受AnyObject?types的参数AnyObject?

在Swift中做这个最好的方法是什么? 有没有办法避免发送不必要的对象与基于对象的注册?

我在一个游乐场尝试了这个,它完美的工作:

 class UndoResponder: NSObject { func myMethod() { println("Undone") } } var undoResponder = UndoResponder() var undoManager = NSUndoManager() undoManager.registerUndoWithTarget(undoResponder, selector: Selector("myMethod"), object: nil) undoManager.undo() 

OS X 10.11+ / iOS 9+更新

(在Swift 3中也是一样的)

OS X 10.11和iOS 9引入了一个新的NSUndoManager函数:

 public func registerUndoWithTarget<TargetType>(target: TargetType, handler: TargetType -> ()) 

设想一个视图控制器(在本例中为self ,types为MyViewController )和一个存储属性namePerson模型对象。

 func setName(name: String, forPerson person: Person) { // Register undo undoManager?.registerUndoWithTarget(self, handler: { [oldName = person.name] (MyViewController) -> (target) in target.setName(oldName, forPerson: person) }) // Perform change person.name = name // ... } 

警告

如果你发现你的撤销不是(即执行,但似乎没有任何事情发生,就好像撤消操作运行,但它仍然显示你想撤消的值),仔细考虑什么值(旧名称在上面的例子中)实际上是在执行undo处理程序的时候。

任何您想要恢复的旧值 (如本例中的oldName )都必须在捕获列表中捕获。 也就是说,如果上面例子中封闭的单行代替:

 target.setName(person.name, forPerson: person) 

… undo不起作用,因为在执行undo处理程序时, person.name被设置为名称,这意味着当用户执行撤消操作时,您的应用程序(在上面的简单情况下)看起来像没有什么,因为它将名称设置为当前的值 ,这当然不会撤消任何东西。

在签名( (MyViewController) -> () )之前的捕获列表( [oldName = person.name] )声明person.name ,以便在声明闭包时引用person.name ,而不是在执行时。

有关捕获列表的更多信息

有关捕获列表的更多信息,Erica Sadun撰写了一篇名为Swift:在closures中捕获引用的文章。 她提到的保留周期问题也值得关注。 另外,尽pipe她没有在她的文章中提到过,但是我在上面使用的捕获列表中的内联声明来自Swift 2.0的Swift编程语言手册的Expressions部分。

其他方法

当然,更详细的方法是let oldName = person.name在调用registerUndoWithTarget(_:handler:) ,然后在范围中自动捕获let oldName = person.name 。 我发现捕获列表的方法更容易阅读,因为它正好与处理程序。

我也完全没有得到registerWithInvocationTarget()发挥非NSObjecttypes(如Swift enum )作为参数很好。 在后一种情况下,请记住, 调用目标不仅要从NSObjectinheritance,而且还应该调用调用目标 上的函数参数 。 或者至less是桥接到Cocoatypes的types(如StringNSStringIntNSNumber等)。 但是,调用目标不被保留,我也无法解决。 此外,使用闭包作为完成处理程序是非常迅速。

在closures(得到它?)

搞清楚这一切,花了我几个小时的勉强控制的愤怒(可能是我的苹果手表关于我的心跳的一部分关心 – “ 窃听!老兄…一直在听你的心,你可能要打坐或者其他的东西”)。 我希望我的痛苦和牺牲帮助。 🙂

更新2:在Xcode 6.1中的Swift使得撤销pipe理器成为一个可选项,所以你可以这样调用prepareWithInvocationTarget():

 undoManager?.prepareWithInvocationTarget(myTarget).insertSomething(someObject, atIndex: index) 

更新:Swift在Xcode6 beta5中简化了对undo manager的prepareWithInvocationTarget()的使用。

 undoManager.prepareWithInvocationTarget(myTarget).insertSomething(someObject, atIndex: index) 

以下是beta4所需要的:


基于NSInvocation的撤消pipe理器API仍然可以使用,但起初并不明显,如何调用它。 我解决了如何使用以下方法成功调用它:

 let undoTarget = undoManager.prepareWithInvocationTarget(myTarget) as MyTargetClass? undoTarget?.insertSomething(someObject, atIndex: index) 

具体而言,您需要将prepareWithInvocationTarget()的结果prepareWithInvocationTarget()转换为目标types,但请记住使其成为可选项,否则会导致崩溃(在beta4上)。 然后你可以调用你想要在撤消堆栈上logging的调用types。

还要确保您的调用目标types从NSObjectinheritance。

我认为这将是Swiftiest如果NSUndoManager接受撤消注册封闭。 这个扩展将有助于:

 private class SwiftUndoPerformer: NSObject { let closure: Void -> Void init(closure: Void -> Void) { self.closure = closure } @objc func performWithSelf(retainedSelf: SwiftUndoPerformer) { closure() } } extension NSUndoManager { func registerUndo(closure: Void -> Void) { let performer = SwiftUndoPerformer(closure: closure) registerUndoWithTarget(performer, selector: Selector("performWithSelf:"), object: performer) //(Passes unnecessary object to get undo manager to retain SwiftUndoPerformer) } } 

那么你可以快速注册任何closures:

 undoManager.registerUndo { self.myMethod() } 

如果需要支持10.10,setValue forKey可以在OS X上实现。 我无法直接设置prepareWithInvocationTarget,因为它返回一个代理对象。

 @objc enum ImageScaling : Int, CustomStringConvertible { case FitInSquare case None var description : String { switch self { case .FitInSquare: return "FitInSquare" case .None: return "None" } } } private var _scaling : ImageScaling = .FitInSquare dynamic var scaling : ImageScaling { get { return _scaling } set(newValue) { guard (_scaling != newValue) else { return } undoManager?.prepareWithInvocationTarget(self).setValue(_scaling.rawValue, forKey: "scaling") undoManager?.setActionName("Change Scaling") document?.beginChanges() _scaling = newValue document?.endChanges() } } 

我试了两天,让Joshua Nozzi的答案在Swift 3中工作,但不pipe我做了什么,价值观都没有被捕获。 请参阅: NSUndoManager:捕获参考types可能吗?

我放弃了,只是通过跟踪撤销和重做堆栈中的更改来自己pipe理它。 所以,给一个人的对象,我会做类似的事情

 protocol Undoable { func undo() func redo() } class Person: Undoable { var name: String { willSet { self.undoStack.append(self.name) } } var undoStack: [String] = [] var redoStack: [String] = [] init(name: String) { self.name = name } func undo() { if self.undoStack.isEmpty { return } self.redoStack.append(self.name) self.name = self.undoStack.removeLast() } func redo() { if self.redoStack.isEmpty { return } self.undoStack.append(self.name) self.name = self.redoStack.removeLast() } } 

然后调用它,我不担心传递参数或捕获值,因为撤销/重做状态是由对象本身pipe理的。 所以说你有一个ViewController来pipe理你的Person对象,你只需调用registerUndo并传递nil

 undoManager?.registerUndo(withTarget: self, selector:#selector(undo), object: nil)