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使用详解

代码传送

学习最好的方式就是让自己当自己的老师.