如何在Swift中注册NSUndoManager?

Kev*_*fin 11 nsundomanager ios swift

我如何UndoManager在Swift中使用?

这是我尝试复制的Objective-C示例:

[[undoManager prepareWithInvocationTarget:self] myArgumentlessMethod];
Run Code Online (Sandbox Code Playgroud)

然而,Swift似乎没有NSUndoManager,这(看似)意味着我不能调用NSInvocation它没有实现的方法.

我在Swift中尝试过基于对象的版本,但它似乎崩溃了我的Playground:

undoManager.registerUndoWithTarget(self, selector: Selector("myMethod"), object: nil)
Run Code Online (Sandbox Code Playgroud)

然而它似乎崩溃,即使我的对象接受类型的参数 undoManager

在Swift中执行此操作的最佳方法是什么?有没有办法避免使用基于对象的注册发送不必要的对象?

Jos*_*zzi 24

OS X 10.11+/iOS 9+更新

(在Swift 3中也一样)

OS X 10.11和iOS 9引入了一项新NSUndoManager功能:

public func registerUndoWithTarget<TargetType>(target: TargetType, handler: TargetType -> ())
Run Code Online (Sandbox Code Playgroud)

想象一下视图控制器(self在此示例中为类型MyViewController)和Person具有存储属性的模型对象name.

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

    // ...

}
Run Code Online (Sandbox Code Playgroud)

警告

如果您发现您的撤消不(即,它执行了,但没有什么似乎已经发生了,就好像撤消操作运行,但它仍然显示你想撤消值),仔细考虑一下值(旧名在上面的例子中)实际上是在执行撤销处理程序闭包时.

您想要还原的任何旧值(如oldName本示例中所示)必须在捕获列表中捕获.也就是说,如果上面例子中的闭包的单行是:

target.setName(person.name, forPerson: person)
Run Code Online (Sandbox Code Playgroud)

...撤消不起作用,因为在执行撤销处理程序关闭时,person.name设置为名称,这意味着当用户执行撤消时,您的应用程序(在上面的简单情况下)似乎什么都不做,因为它是将名称设置为其当前值,这当然不会撤消任何内容.

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

有关捕获列表的更多信息

有关捕获列表的更多信息,Erica Sadun撰写了一篇名为Swift的文章:在闭包中捕获引用.同样值得关注她提到的保留周期问题.此外,虽然她在她的文章中没有提到它,但我在上面使用它时捕获列表中的内联声明来自Swift编程语言的Swift 2.0的表达式部分.

其他方法

当然,更简洁的方法是let oldName = person.name在你的调用之前registerUndoWithTarget(_:handler:),然后oldName在范围内自动捕获.我发现捕获列表方法更容易阅读,因为它与处理程序一致.

我也完全没能registerWithInvocationTarget()与非NSObject类型(如Swift enum)作为参数玩得很好.在后一种情况下,请记住,不仅应该从调用目标继承NSObject,而且还要调用在该调用目标上调用的函数参数.或至少是类型的桥可可类型(如StringNSStringIntNSNumber,等等).但是也存在一些问题,即调用目标没有被保留,我无法解决.此外,使用闭包作为完成处理程序更加迅速.

在结束时(得到它?)

弄清楚所有这些让我花费了几个小时的勉强控制的愤怒(可能我的Apple Watch关于我的心率有些担忧 - " 点击水龙头!老兄......一直在听你的心脏,你可能想要冥想或者其他的东西").我希望我的痛苦和牺牲有所帮助.:-)


Chr*_*les 6

更新2:Xcode 6.1中的Swift已经undoManager可选,因此您可以像这样调用prepareWithInvocationTarget():

undoManager?.prepareWithInvocationTarget(myTarget).insertSomething(someObject, atIndex: index)
Run Code Online (Sandbox Code Playgroud)

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

undoManager.prepareWithInvocationTarget(myTarget).insertSomething(someObject, atIndex: index)
Run Code Online (Sandbox Code Playgroud)

以下是beta4所需的内容:


基于NSInvocation的撤消管理器API仍可以使用,虽然它不是在第一个明显的如何调用它.我使用以下方法计算出如何成功调用它:

let undoTarget = undoManager.prepareWithInvocationTarget(myTarget) as MyTargetClass?
undoTarget?.insertSomething(someObject, atIndex: index)
Run Code Online (Sandbox Code Playgroud)

具体来说,您需要将结果prepareWithInvocationTarget()转换为目标类型,但请记住将其设置为可选,否则会导致崩溃(无论如何都会在beta4上).然后,您可以使用要在撤消堆栈上记录的调用来调用您键入的可选项.

还要确保您的调用目标类型继承自NSObject.

  • 确保您呼叫的功能不是私密的. (2认同)

Cez*_*zar 3

我在操场上尝试过,效果完美:

class UndoResponder: NSObject {
    @objc func myMethod() {
        print("Undone")
    }
}

var undoResponder = UndoResponder()
var undoManager = UndoManager()
undoManager.registerUndo(withTarget: undoResponder, selector: #selector(UndoResponder.myMethod), object: nil)
undoManager.undo()
Run Code Online (Sandbox Code Playgroud)