Undo-Redo操作

撤销Undo/重做Redo 的操作支持是编辑/设计类应用中一项非常重要的功能。

Cocoa对Undo/Redo操作做了统一封装,提供NSUndoManager管理类,对应用开发提供了系统级Undo/Redo管理支持。

NSUndoManager管理了Undo和Redo 2个操作堆栈,应用将每个最小的操作的状态(每一个历史状态)注册到Undo栈。执行Undo操作时,从Undo栈获取顶部的历史状态,进行还原操作。

Redo操作则是一个逆向过程,每次Undo操作之前需要将当前最新的状态存储到Redo栈。然后执行Redo操作,即从Redo栈获取一个状态执行复原操作。

正常的每一步操作过程操作时,需要将当前操作的上一个状态压入Undo栈。Undo操作执行之前需要将当前堆栈顶部状态的上一个状态存入到Redo堆栈,Redo操作执行之前需要将当前状态存入到Undo栈,这是最关键的一点。

Undo/Redo流程分析

我们从绘图程序的一个简单场景来分析下整个Undo/Redo过程。

1.新建一个矩形,填充色默认为黑色

新建一个矩形的上一个状态是删除,因此Undo栈状态如下:Redo栈为空。

UndoOp1

2.修改矩形填充色为红色

修改填充色为红色,上一个状态是填充色为黑色,将修改填充色为黑色压入Undo栈,Redo栈为空。
执行修改矩形填充色为红色的操作

UndoOp2

3.执行Undo

执行Undo操作,当前状态是填充色为红色,将其压入Redo栈;执行修改填充色为黑色的操作。

UndoOp3

4.执行Undo

再执行Undo操作,当前状态是新建矩形,将其压入Redo栈。执行删除矩形操作

UndoOp4

5.执行Redo

执行Redo,当前状态是删除矩形,将其压入Undo栈。执行新建矩形操作

UndoOp5

6.执行Redo

执行Redo,当前状态是填充色为黑色,将其压入Undo栈。执行修改矩形为红色的操作

UndoOp6

从上面绘图操作Undo/Redo操作流程来看,所有的状态可以归结为2类:

1)对象创建/删除,增/删互为逆操作

2)对象属性的修改,属性的新/旧值为对应为逆操作

Undo/Redo流程总结如下:

正常的操作中(没有Undo/Redo操作)时,将马上要执行的操作的逆操作压入Undo Stack。

请求执行Undo操作时,先从Undo Stack获取Undo操作的动作, 将其逆向操作压入Redo栈,然后执行Undo的具体操作。

请求执行Redo操作时,从Redo Stack获取Redo操作的动作,将其逆向操作压入Undo栈,然后执行Redo的具体操作。

实现原理

NSInvocation 是一种包含执行方法的对象,方法签名和参数组合的对象。

NSInvocation = target + selector +parameters。

借助NSInvocation对象,可以将其他任何对象实例方法和参数存储起来,以备需要的时候执行。

NSUndoManager将需要Undo的操作封装成NSInvocation对象存储到Undo堆栈。需要撤销操作时,从Undo堆栈Top顶部获取一个NSInvocation对象,执行Undo操作。同时将其逆操作封装成NSInvocation对象,压入到Redo堆栈。需要Redo操作执行类似的过程。

NSInvocationStack

NSUndoManager提供了注册操作到Undo/Redo Stack的接口,应用在需要的流程中调用注册接口将操作压栈。按正常的操作,请求Undo的操作,请求Redo的操作 3种不同的情况区分堆栈的管理的职责。

正常的操作由应用开发者负责调用NSUndoManager接口压入Undo 栈;请求Undo的操作由NSUndoManager负责触发Undo 的执行,并将Undo操作的逆向操作压入Redo栈;请求Redo的操作由NSUndoManager负责触发Redo 的执行,并将Redo操作的逆向操作压入Undo栈。

下面是一个修改填充色的示例代码

- (void)changFillColor:(NSColor*)color {
    NSString *oldFillColor = self.fillColor;
    NSUndoManager* undoManager = [self undoManager];
    //将修改前操作注册到栈(以对象self,方法changFillColor,参数oldFillColor构造NSInvocation实例对象)
    [[undoManager prepareWithInvocationTarget:self]changFillColor:oldFillColor ];
    self.fillColor = color;
    [self reDraw];
}

正常流程调用changFillColor修改填充色的方法时,会调用prepareWithInvocationTarget方法将老的颜色oldFillColor参数等构造NSInvocation, 此时NSUndoManager会判断当前操作是否由Undo/Redo调用触发,如果不是则压入Undo 栈,否则按下面流程继续处理:

请求Undo的操作时,再次执行changFillColor方法,类似的构造NSInvocation,此时NSUndoManager会判断当前的操作为Undo执行changFillColor方法的流程触发,则会将NSInvocation压入Redo 栈。

请求Redo的操作时跟上面请求Undo类似。

同样的方法changFillColor,在不同的调用链场景情况下NSUndoManager会智能判断存入不同的栈。这种巧妙的设计,减轻了编程的复杂度。

Undo/Redo Action的管理

NSUndoManager的创建

所有基于NSResponder的子类,NSApplication,NSPopover,NSView,NSViewController,NSWindow,NSWindowController系统有默认undoManager属性变量,因此一般情况可以直接使用,不需要再手工创建。
NSDocument类也包括undoManager属性,可以直接使用。

一句话,大多数场景下你可以直接使用系统对象的undoManager无需自己单独创建。只有除了这些类之外的类中需要实现Undo/Redo操作时才需要自己创建NSUndoManager类。

注册Undo Action

有2种注册Undo Action事件到NSUndoManager

1.使用registerUndoWithTarget方法,以对象-方法-参数形式注册

[self.undoManager registerUndoWithTarget:self
                selector:@selector(setMyObjectTitle:)
                object:currentTitle];

2.NSInvocation形式注册

[[self.undoManager prepareWithInvocationTarget:self]
                setMyObjectWidth:currentWidth height:currentHeight];

第一种方式参数比较固定只能是对象形式,第二种方法比较灵活可以是任意参数组合。

清除Undo Action

执行removeAllActions删除undo堆栈中所有的Action;执行removeAllActionsWithTarget,删除指定target的Action。

NSUndoManager会retain压栈的对象,因此当一个对象被删除或者没有意义存在时,请务必同步清除注册到undoManager中的Action。

禁止注册Undo Action

默认情况下NSUndoManager属性undoRegistrationEnabled为YES,允许注册Undo Action。

你可以使用disableUndoRegistration方法禁止注册Undo Action。注册大量的Undo Action会消耗大量的内存,一般对大量无意义的操作可以临时关闭Undo Action的注册。比如说在屏幕上拖放一个对象时,对变化的鼠标坐标,就不需要Undo注册。只需要对拖放结束后的新的位置坐标注册Undo Action即可。

Undo Action 命名

调用NSUndoManager的setActionName方法给Undo Action 命名。给每个Action取一个有意义的名字,执行Undo/Redo操作时可以用来显示当前的具体操作。

Undo Group

可以将多个连续的操作定义为一个组,执行Undo/Redo时,执行当前组内的所有的Action操作。

NSUndoManager执行beginUndoGrouping创建一个组,执行endUndoGrouping关闭组。

beginUndoGrouping和endUndoGrouping必须成对出现。

创建了组在关闭之前,后续所有加入Undo Stack中的操作,被做为一个整体在执行NSUndoManager的Undo撤销操作时全部执行。

默认情况下不需要手工创建管理组,NSUndoManager自动为每一步操作创建一个单独的组。

Undo Stack的深度

可以设置levelsOfUndo属性来定义Undo/Redo Stack的深度。当注册的Undo/Redo数超过levelsOfUndo时,堆栈底部存储的操作对象将被删除。

Undo-Redo 通知消息

执行Undo/Redo操作 在不同流程系统定义了各种通知消息

执行beginUndoGrouping,endUndoGrouping后触发的所有通知消息

NSUndoManagerDidOpenUndoGroupNotification
NSUndoManagerCheckpointNotification
NSUndoManagerWillCloseUndoGroupNotification
NSUndoManagerDidCloseUndoGroupNotification

执行Undo 操作触发的所有消息

NSUndoManagerCheckpointNotification
NSUndoManagerCheckpointNotification
NSUndoManagerWillUndoChangeNotification
NSUndoManagerCheckpointNotification
NSUndoManagerDidUndoChangeNotification

执行Redo 操作触发的所有消息

NSUndoManagerCheckpointNotification
NSUndoManagerWillRedoChangeNotification
NSUndoManagerCheckpointNotification
NSUndoManagerCheckpointNotification
NSUndoManagerCheckpointNotification
NSUndoManagerDidRedoChangeNotification

Undo-Redo编程示例

以最简单的一个数值计算来演示Undo/Redo的基本使用。
需求:2个数相加,计算求和。用户可以输入加数和被加数,点击计算按钮求和。同时提供Undo,Redo功能。

设计界面如下

ComputeUndoRedo

属性变量和Outlet绑定变量

@interface NSUndoWindowController ()

@property (weak) IBOutlet NSTextField *firstAddParaTextField;
@property (weak) IBOutlet NSTextField *secondAddParaTextField;
@property (weak) IBOutlet NSTextField *sumTextField;

@property(nonatomic,assign)NSInteger para1;
@property(nonatomic,assign)NSInteger para2;

@property (nonatomic,strong)NSUndoManager *undoManager;
@end

window加载初始化

- (void)windowDidLoad {
    [super windowDidLoad];
    self.para1 = 1;
    self.para2 = 2;
    [self computePara1:1 para2:2];
}

按钮事件函数

//计算按钮事件
- (IBAction)computeAction:(id)sender {
    NSString *firstAddParaText = self.firstAddParaTextField.stringValue;
    NSString *secondAddParaText = self.secondAddParaTextField.stringValue;
    NSInteger para1 = [firstAddParaText integerValue];
    NSInteger para2 = [secondAddParaText integerValue];
    [self computePara1:para1 para2:para2];
}

//Undo按钮对应的事件
- (IBAction)undoAction:(id)sender {
    //请求执行Undo
    [self.undoManager undo];
}

//Redo按钮对应的事件
- (IBAction)redoAction:(id)sender {
   //请求执行Redo
    [self.undoManager redo];
}

执行计算的处理函数

- (void)computePara1:(NSInteger)para1 para2:(NSInteger)para2 {
    
    if(self.para1 == para1 && self.para2 == para2){
        
    }
    else{
        //注册Undo操作
        [[self.undoManager prepareWithInvocationTarget:self]computePara1:self.para1  para2:self.para2 ];
        NSString *title = [NSString stringWithFormat:@"%ld+%ld",self.para1,self.para2];
        [self.undoManager setActionName:title];
    }
    
    self.para1 = para1;
    self.para2 = para2;
    
    NSString *firstAddPara  = [NSString stringWithFormat:@"%ld",self.para1];
    NSString *secondAddPara  = [NSString stringWithFormat:@"%ld",self.para2];
    NSInteger sum = para1 + para2;
    
    self.firstAddParaTextField.stringValue  =  firstAddPara;
    self.secondAddParaTextField.stringValue =  secondAddPara;
    self.sumTextField.stringValue = [NSString stringWithFormat:@"%ld",sum];
}

没有自己去创建NSUndoManager实例,应用中undoManager使用了window的undoManager

- (NSUndoManager*)undoManager {
    if(!_undoManager){
        _undoManager = self.window.undoManager;
    }
    return _undoManager;
}