Cocoa中的NSUndoManager

原文链接:http://nshipster.com/nsundomanager/

前言

Foundation框架中的NSUndoManager为我们提供了去撤销重复操作的健壮API。

认的话,每个应用窗口都有一个撤销管理者,并且在响应者链的任意对象可以管理一个自定义的撤销管理从而实现对本地各自视图撤销和重复操作。除了UITextFieldUITextArea自动配有撤销功能之外,其余对苹果开发者都是一个待修的课题。

撤销操作

要想进行一个动作撤销,要先注册一个“撤销操作”。

注册一个简单的撤销操作

调用NSUndomanger -registerUndoWithTarget:selector:object:到要执行撤销操作的对象上。同时指定一个撤销动作的名字,使用NSUndoManager -setActionName:。撤销对话框展示动作的名字,所以必须本地化。

func updatescore(score: NSNumber) {
    undoManager.registerUndoWithTarget(self,selector:Selector("updatescore:"),object:score)
    undoManager.setActionName(NSLocalizedString("actions.update",comment: "Update score"))
    myMovie.score = score
}

通过NSInvocation注册一个复杂的撤销操作

简单得撤销操作可能让功能过于呆板了,因为撤销一个动作可能需要的参数不止一个在这种情况下,我们可以拔高通过NSInvocation来记录选择器与必要的参数。调用prepareWithInvocationTarget:记录哪个对象将要收到XX将要变化的消息。

func movePiece(piece: Chesspiece,row:UInt,column:UInt) {
    let undoController : ViewController = undoManager?.prepareWithInvocationTarget(self) as ViewController
    undoController.movePiece(piece,row:piece.row,column:piece.column)
    undoManager?.setActionName(NSLocalizedString("actions.move-piece","Move Piece"))

    piece.row = row
    piece.column = column
    updateChessboard()
}

这里的原理是NSUndoManager实现了forwardInvocation:。在撤销管理者收到撤销-movePiece:row:column:的消息的时候,因为自身没有实现这个方法的缘故它将这个消息丢给了目标对象。

执行撤销

注册完撤销操作以后就可以愉快的使用NSUndoManager -undo and NSUndoManager -redo进行撤销重做进行玩耍了。

与iOS摇晃手势的交互

认的,用户在摇晃设备的时候出发撤销操作。如果视图控制器需要响应撤销请求,那么视图控制器需要:

  • 能够成为第一响应者
  • 当视图出现得时候成为第一个响应者
  • 当视图消失的时候脱离第一响应者

当视图控制器接收到动作事件,操作系统会弹出一个对话框让用户选择是不是执行撤销或者重做的动作。(如果有这个功能的话)视图控制器的undoManager属性会完成用户做好选择后的活儿。

class ViewController: UIViewController {
    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)
        becomeFirstResponder()
    }

    override func viewWilldisappear(animated: Bool) {
        super.viewWilldisappear(animated)
        resignFirstResponder()
    }

    override func canBecomeFirstResponder() -> Bool {
        return true
    }

    // ...
}

自定义撤销栈

组动作

所有的撤销操作注册到同一个run loop中会导致都不执行,除非指定一个“撤销组”。组的概念让撤销和重做的动作可以同时一起进行。尽管所有的动作可以被单独的执行以及撤销,如果用户同时执行两个操作,同时撤销两个有利于保留用户体验一致性。

func readAndArchiveEmail(email: Email) {
    undoManager?.beginUndoGrouping()
    markEmail(email,read: true)
    archiveEmail(email)
    undoManager?.setActionName(NSLocalizedString("actions.read-archive",comment:"Mark as Read and Archive"))
    undoManager?.endUndoGrouping()
}

func markEmail(email: Email,read:Bool) {
    let undoController: ViewController = undoManager?.prepareWithInvocationTarget(self) as ViewController
    undoController.markEmail(email,read:email.read)
    undoManager?.setActionName(NSLocalizedString("actions.read",comment:"Mark as Read"))
    email.read = read
}

func archiveEmail(email: Email) {
    let undoController: ViewController = undoManager?.prepareWithInvocationTarget(self) as ViewController
    undoController.moveEmail(email,toFolder:"InBox")
    undoManager?.setActionName(NSLocalizedString("actions.archive",comment:"Archive"))
    moveEmail(email,toFolder:"All Mail")
}

清空栈

有时候需要清理撤销管理者的动作列表来避免造成用户混淆从而导致其预期不到的结果。最常用的方式是党上下文动态变化的时候,比如改变iOS上可见的视图控制器或是打开了一个文件夹。当此类情况发生的时候,我们可以调用NSUndoManager -removeAllActions or NSUndoManager -removeAllActionsWithTarget:来清理我们的撤销管理者栈。

警告

如果一个动作对应多个撤销与重做,要核对所设置的操作名称以确保撤销对话框所反映的行动将被撤销称号的前撤消操作是否已经发生。下面栗子展示一对反操作,比如添加删除对象:

func addItem(item: NSObject) {
    undoManager?.registerUndoWithTarget(self,selector: Selector("removeItem:"),object:item)
    if undoManager?.undoing == false {
        undoManager?.setActionName(NSLocalizedString("action.add-item",comment: "Add Item"))
    }
    myArray.append(item)
}

func removeItem(item: NSObject) {
    if let index = find(myArray,item) {
        undoManager?.registerUndoWithTarget(self,selector: Selector("addItem:"),object:item)
        if undoManager?.undoing == false {
            undoManager?.setActionName(NSLocalizedString("action.remove-item",comment: "Remove Item"))
        }
        myArray.removeAtIndex(index)
    }
}

如果你使用像是Kiwi的测试框架,那么请在用例teardown中进行撤销栈的清除。不然其他的测试用例将会共享当前用例的撤销状态并且用例中调用NSUndoManager -undo会导致意想不到的结果。

相关文章

我正在用TitaniumDeveloper编写一个应用程序,它允许我使用Ja...
我的问题是当我尝试从UIWebView中调用我的AngularJS应用程序...
我想获取在我的Mac上运行的所有前台应用程序的应用程序图标....
我是一名PHP开发人员,我使用MVC模式和面向对象的代码.我真的...
OSX中的SetTimer在Windows中是否有任何等效功能?我正在使用...
我不确定引擎盖下到底发生了什么,但这是我的设置,示例代码和...