撤销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过程。
1.新建一个矩形,填充色默认为黑色
新建一个矩形的上一个状态是删除,因此Undo栈状态如下:Redo栈为空。
2.修改矩形填充色为红色
修改填充色为红色,上一个状态是填充色为黑色,将修改填充色为黑色压入Undo栈,Redo栈为空。
执行修改矩形填充色为红色的操作
3.执行Undo
执行Undo操作,当前状态是填充色为红色,将其压入Redo栈;执行修改填充色为黑色的操作。
4.执行Undo
再执行Undo操作,当前状态是新建矩形,将其压入Redo栈。执行删除矩形操作
5.执行Redo
执行Redo,当前状态是删除矩形,将其压入Undo栈。执行新建矩形操作
6.执行Redo
执行Redo,当前状态是填充色为黑色,将其压入Undo栈。执行修改矩形为红色的操作
从上面绘图操作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操作执行类似的过程。
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会智能判断存入不同的栈。这种巧妙的设计,减轻了编程的复杂度。
所有基于NSResponder的子类,NSApplication,NSPopover,NSView,NSViewController,NSWindow,NSWindowController系统有默认undoManager属性变量,因此一般情况可以直接使用,不需要再手工创建。
NSDocument类也包括undoManager属性,可以直接使用。
一句话,大多数场景下你可以直接使用系统对象的undoManager无需自己单独创建。只有除了这些类之外的类中需要实现Undo/Redo操作时才需要自己创建NSUndoManager类。
有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];
第一种方式参数比较固定只能是对象形式,第二种方法比较灵活可以是任意参数组合。
执行removeAllActions删除undo堆栈中所有的Action;执行removeAllActionsWithTarget,删除指定target的Action。
NSUndoManager会retain压栈的对象,因此当一个对象被删除或者没有意义存在时,请务必同步清除注册到undoManager中的Action。
默认情况下NSUndoManager属性undoRegistrationEnabled为YES,允许注册Undo Action。
你可以使用disableUndoRegistration方法禁止注册Undo Action。注册大量的Undo Action会消耗大量的内存,一般对大量无意义的操作可以临时关闭Undo Action的注册。比如说在屏幕上拖放一个对象时,对变化的鼠标坐标,就不需要Undo注册。只需要对拖放结束后的新的位置坐标注册Undo Action即可。
调用NSUndoManager的setActionName方法给Undo Action 命名。给每个Action取一个有意义的名字,执行Undo/Redo操作时可以用来显示当前的具体操作。
可以将多个连续的操作定义为一个组,执行Undo/Redo时,执行当前组内的所有的Action操作。
NSUndoManager执行beginUndoGrouping创建一个组,执行endUndoGrouping关闭组。
beginUndoGrouping和endUndoGrouping必须成对出现。
创建了组在关闭之前,后续所有加入Undo Stack中的操作,被做为一个整体在执行NSUndoManager的Undo撤销操作时全部执行。
默认情况下不需要手工创建管理组,NSUndoManager自动为每一步操作创建一个单独的组。
可以设置levelsOfUndo属性来定义Undo/Redo Stack的深度。当注册的Undo/Redo数超过levelsOfUndo时,堆栈底部存储的操作对象将被删除。
执行Undo/Redo操作 在不同流程系统定义了各种通知消息
NSUndoManagerDidOpenUndoGroupNotification
NSUndoManagerCheckpointNotification
NSUndoManagerWillCloseUndoGroupNotification
NSUndoManagerDidCloseUndoGroupNotification
NSUndoManagerCheckpointNotification
NSUndoManagerCheckpointNotification
NSUndoManagerWillUndoChangeNotification
NSUndoManagerCheckpointNotification
NSUndoManagerDidUndoChangeNotification
NSUndoManagerCheckpointNotification
NSUndoManagerWillRedoChangeNotification
NSUndoManagerCheckpointNotification
NSUndoManagerCheckpointNotification
NSUndoManagerCheckpointNotification
NSUndoManagerDidRedoChangeNotification
以最简单的一个数值计算来演示Undo/Redo的基本使用。
需求:2个数相加,计算求和。用户可以输入加数和被加数,点击计算按钮求和。同时提供Undo,Redo功能。
设计界面如下
属性变量和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;
}