NSUndoManager的理解及使用
以下内容均为个人总结理解,如有错误欢迎指出
NSUndoManager总结
NSUndoManager是苹果提供的可以撤销(undo)恢复(redo)的一套API,他的使用方法呢看文档理解起来很难,然后看网络上的内容也有很多的介绍,但是看的多了发现好多都是从一篇文章中衍生出来的.写的很好,但是可能有些点写的不详细,下面是个人总结,希望对诸位有帮助.
本来想直接写个人总结的精髓,但是发现没有铺垫下不了笔,要是直接写结果估计就该骂我写的烂了,所以还是理一遍主要思路,详细的API简介可以参考下面的这篇文章
ForeverGuard-NSUndoManager
开始正文,NSUndoManager是UIResponder的公开的一个属性,有些人说是成员变量特意去看了一下API确认不是成员变量,他们还是有区别的具体点我,所以说UIResponder的子类都有这个东西,其他的可以在使用时自行摸索尝试.
NSUndoManger内部有两个栈,undo栈(撤销)和redo栈(重写,恢复)
在UIResponder中NSUndoManager是readonly只读属性,所以我们要使用需要自己初始化
1.初始化一个NSUndoManager
NSUndoManager * undoManager = [[NSUndoManager alloc]init];
2.注册操作到undo栈中
好了到这里其实已经到重点了,就是注册的时候到底注册的是什么呢?下面直接上代码分析
undo注册的方法应该是一个反向操作,下面代码见分析
#import "UndoManager.h"
@implementation UndoManager
{
NSUndoManager * undoManager;
//测试数组
NSMutableArray * titleArr;
}
- (instancetype)init
{
self = [super init];
if (self) {
//初始话undoManager
undoManager = [[NSUndoManager alloc]init];
//初始话测试数组
titleArr = [NSMutableArray new];
//接下来就开始测试了
//第一步,先看addTitleWithStr:这个方法里面的简述
[self addTitleWithStr:@"栈1"];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(2);
[self addTitleWithStr:@"栈2"];
});
//第二步
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(4);
//执行撤销操作,判断是否能撤销
if ([self->undoManager canUndo]) {
//undo这个方法最终调用的是undo栈顶存入的方法也就是removeTitle:这个方法,下面请看removeTitle:方法里面的简述
[self->undoManager undo];
}
NSLog(@"titleArr:%@",self->titleArr);
//执行恢复操作,判断是否能撤销
if ([self->undoManager canRedo]) {
[self->undoManager redo];
}
NSLog(@"titleArr:%@",self->titleArr);
});
}
return self;
}
- (void)addTitleWithStr:(NSString *)str{
//执行的操作每一步都需要registerUndoWithTarget一次,将方法和参数都放到undo栈中,划重点->注册的是逆向操作删除,注册的是逆向操作删除,注册的是逆向操作删除
//当执行undo(撤销)操作时我们需要从undo栈顶取出存入的操作并执行,我们这个方法是存入,所以相应的反操作就是删除
[undoManager registerUndoWithTarget:self selector:@selector(removeTitle:) object:str];
//从这里可以看到NSObject也可以注册到NSUndoManager,本类是NSObject类
//然后将数据添加到数组,看到这从init方法继续看不要直接跳到removeTitle:方法
[titleArr addObject:str];
}
- (void)removeTitle:(NSString *)str{
//这个removeTitle:方法就是对应的撤销操作,不会调用removeTitle:这个方法,调用这个方法是[undoManager undo]
//在这里我们还需要注册一次addTitleWithStr:,为什么呢?
//原因是一个规则,就是当执行[undoManager undo]操作时,执行registerUndoWithTarget:方法时,注册的这个内容会存到redo的栈中.
//所以我们接下来执行[undoManager redo]操作时会调用addTitleWithStr:这个方法,依次类推会一直循环下去
//这里呢也证明了一点,执行[undoManager undo]操作时undo栈顶的会出栈
//执行[undoManager redo]操作时redo栈顶的也会出栈
[undoManager registerUndoWithTarget:self selector:@selector(addTitleWithStr:) object:str];
[titleArr removeObject:str];
}
@end
上面代码中我使用线程操作是为了告诉各位一件事,就是说undo栈和redo栈其实对添加进来的方法是有进一步的包装的,在一个runloop执行完毕时,把这一个runloop期间添加进栈的所有操作包到一起形成一个集合,执行操作时是对这个集合进行操作的.
举个例子就是redo或undo栈就是一个数组A,然后数组A里面包含多个数组,每个数组里面放的是一个runloop周期内加进来的方法.每次执行undo或redo操作时,操作的是A数组里面的小数组里面的所有操作.当然如果你觉得在一个runloop周期内你的操作不能执行完,比如画板涂鸦绘画的过程很长绝对不是一个runloop能解决的,那么可以使用[undoManager beginUndoGrouping];[undoManager endUndoGrouping];这两个方法,在这两个方法之间所有注册的undo里面的都会放到小数组里面,下次撤销或恢复时都会一起执行的.
还有部分理解没有写入,夜很深了,睡一觉醒来再接着码;
将方法注册到undo栈的方式有三种,下面依次介绍
(1).selector方式
使用- (void)registerUndoWithTarget:(id)target selector:(SEL)selector object:(nullable id)anObject;方法,上面的代码就是使用的这种方式,这种方法有缺点就是参数只能携带一个.详情看上面的代码.
(2).block方式
使用- (void)registerUndoWithTarget:(id)target handler:(void (^)(id target))undoHandle;方法,这种形式就是说把需要进行逆操作的代码放到block块中执行,根据API中的表达可以得知,需要iOS9以后可以使用,方法并没有持有target,但是我们仍需注意循环引用的问题.
- (void)addTitleWithStr:(NSString *)str{
__weak typeof (self) weakSelf = self;
[undoManager registerUndoWithTarget:self handler:^(id _Nonnull target) {
[weakSelf removeTitle:str];
}];
[titleArr addObject:str];
}
- (void)removeTitle:(NSString *)str{
__weak typeof(self) weakSelf = self;
[undoManager registerUndoWithTarget:self handler:^(id _Nonnull target) {
[weakSelf addTitleWithStr:str];
}];
[titleArr removeObject:str];
}
(3).使用NSInvocation和NSUndoManager搭配使用,可以传递多个参数.
NSInvocation使用详解