撤销Undo

“撤销”功能在OS X上很常见,想法就是:假如用户及时意识到犯的错误,可以回退错误的动作。通常,Mac应用会维持内部撤销动作(undoable actions)的栈,选择Edit->Undo或Command-Z来回退该栈顶部的动作,当然,动作也可以重做(redo)。

一些iOS应用,可能也需要有限的撤销功能,当然,这并不难实现。一些内置的视图,特别是涉及到文本输入(text entry)的,比如UITextField和UITextView,已经实现了撤销功能。你可以将撤销功能添加到应用的其它部分。

撤销(undo)功能通过NSUndoManager实例来提供,它仅会维持可撤销动作(redoable actions)的一个栈,和可重做动作(redoable actions)的一个栈。当用户选择撤销最近的动作时,撤销栈(undo stack)顶部的动作(即栈顶动作)就会被弹出(pop),实现回退;弹出的动作会被压进(push)重做栈(redo stack)的顶部。

一.Undo Manager(撤销管理器)

有三种方式可以将一个动作(action)注册成可撤销的(undoable):

1.Target-Action撤销
使用NSUndoManager的方法registerUndoWithTarget:selector:object:,这个方法使用了target-action架构。稍后,如果向NSUndoManager发送undo消息时,就会执行target的action方法。action方法的工作是撤销需要撤销的一切。

先看一段示例代码:

DragView.swift代码:

import UIKit

class DragView: UIView {
    let undoer = NSUndoManager()

    override var undoManager: NSUndoManager?{
       return self.undoer
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        let p = UIPanGestureRecognizer(target: self, action: Selector("dragging:"))
        let l = UILongPressGestureRecognizer(target: self, action: Selector("longPress:"))
        self.addGestureRecognizer(p)
        self.addGestureRecognizer(l)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func setCenterUndoably (newCenter: NSValue) {
        self.undoer.registerUndoWithTarget(self, selector: Selector("setCenterUndoably:"), object: NSValue(CGPoint: self.center))
        self.undoer.setActionName("Move")
        if self.undoer.undoing || self.undoer.redoing {
            UIView.animateWithDuration(0.4, delay: 0.1, options: [], animations: { () -> Void in
                self.center = newCenter.CGPointValue()
                }, completion: nil)
        }else {
            self.center = newCenter.CGPointValue()
        }
    }

    func dragging (p: UIPanGestureRecognizer) {
        switch p.state {
        case .Began:
            self.undoer.beginUndoGrouping()
            fallthrough
        case .Began,.Changed:
            let delta = p.translationInView(self.superview)
            var c = self.center
            c.x += delta.x
            c.y += delta.y
            self.setCenterUndoably(NSValue(CGPoint: c))
            p.setTranslation(CGPointZero, inView: self.superview)
        case .Ended, .Cancelled:
            self.undoer.endUndoGrouping()
            self.becomeFirstResponder()
        default:break
        }
    }

    func longPress(g: UIGestureRecognizer) {
        if g.state == .Began {
            let m = UIMenuController.sharedMenuController()
            m.setTargetRect(self.bounds, inView: self)
            let mi1 = UIMenuItem(title: self.undoer.undoMenuItemTitle, action: "undo:")
            let mi2 = UIMenuItem(title: self.undoer.redoMenuItemTitle, action: "redo:")
            m.menuItems = [mi1,mi2]
            m.setMenuVisible(true, animated: true)
        }
    }

    func undo(_: AnyObject?){
        self.undoer.undo()
    }

    func redo(_: AnyObject?){
        self.undoer.redo()
    }

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

}

使用该DragView:


import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let drag = DragView(frame: CGRectMake(20,20,100,100))
        drag.backgroundColor = UIColor.orangeColor()
        self.view.addSubview(drag)
    }

}

现在讲解下上面的示例代码:

这里我们想撤销的是DragView的center属性,首先使DragView可以拖拽,并且完成self.center设置的动作(action)。接下来让这个动作可撤销的,为了撤销我们将要执行的动作,NSUndoManager需要发送什么消息呢?我们想要NSUndoManager将self.center退回到现在的值。本例中,NSUndoManager调用setCenterUndoably:方法。

调用registerUndoWithTarget方法会将想要撤销回的状态属性放入对应的栈(undo栈或者redo栈)中。

上面代码中,出现了Undo Grouping的概念,对于拖拽这个手势,从触碰到视图到离开视图,dragging:被调用了多次,而每次都会调用setCenterUndoably:方法,所以这样的话,撤销并不是针对整个拖拽手势的,而是一小部分。所以我们用到了Undo Grouping,当手势开始时,开始group;当手势结束时,关闭group。

2.Invocation撤销

此种方法中,涉及到prepareWithInvocationTarget:方法,NSUndoManager使用到动态机制(NSInvocation)。

3.Functional撤销

iOS9引入第三种方式,registerUndoWithTarget:handler:

三种方式的思想是一致的。

二.撤销的界面

我们必须要决定如何让用户请求undo和redo,可以通过两个按钮分别向NSUndoManager发送undo和redo消息。下面来讨论下其它的方式:

1.Shake-To-Edit

默认,应用是支持shake-to-edit。这意味着用户可以摇动设备来产生undo/redo界面。你可以设置共享的UIApplication的applicationSupportsShakeToEdit为false来关闭这个功能。没有关闭该功能时,用户摇动设备时,运行时(runtime)会遍历响应者链,从第一个响应者开始,寻找一个响应者,它继承(即上例子中的override)的undoManager属性返回真正的NSUndoManager实例。如果发现一个,会跳出一个undo/redo界面,允许用户与NSUndoManager交互。

回忆一下,如何让一个UIResponder成为第一响应器:canBecomeFirstResponder必须要返回true,而且,必须要通过方法becomeFirstResponder来成为第一响应器。

为了让shake-to-edit工作,我们必须要为undoManager属性提供getter方法来返回我们的撤销管理器(undo manager)。

效果图:

注意:图中出现的Move是通过self.undoer.setActionName(“Move”)

2.撤销菜单(Undo Menu)

这与UITextField、UITextView展示Copy(复制)和Paste(粘贴)菜单项是相同的。激活这种菜单的要求与shake-to-edit是相同的:我们需要一个响应者链,在它的底部是第一响应者。

回忆一下如何实现菜单:获取全局的单例对象UIMenuController对象UIMenuController.sharedMenuController(),声明一个自定义的UIMenuItem数组作为菜单的menuItems属性。通过向UIMenuController发送setMenuVisible:animated:消息来显示菜单。只有canPerformAction:withSender:方法返回true时,特定的菜单项才会出现在菜单中。

效果图如下:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值