如何在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
)和一个存储属性name
的Person
模型对象。
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()
发挥非NSObject
types(如Swift enum
)作为参数很好。 在后一种情况下,请记住, 调用目标不仅要从NSObject
inheritance,而且还应该调用该调用目标 上的函数的参数 。 或者至less是桥接到Cocoatypes的types(如String
和NSString
或Int
和NSNumber
等)。 但是,调用目标不被保留,我也无法解决。 此外,使用闭包作为完成处理程序是非常迅速。
在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从NSObject
inheritance。
我认为这将是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)