本节书摘来自异步社区《iOS创意程序设计家》一书中的第6章,第6.5节Undo与Redo机制,作者 林柏全,更多章节内容可以访问云栖社区“异步社区”公众号查看
6.5 Undo与Redo机制iOS创意程序设计家还记得第5章在提到UITextView的时候是怎样实现Undo机制的吗?其实,iPhone OS3.0以后就内建了Undo-Redo的机制。在默认的情况下,每一个应用程序的window对象都提供一个NSUndoManager对象,用以管理Undo与Redo的操作,而窗口内的每一个控件也都有其各自的NSUndoManager对象。
这个Undo-Redo机制是怎么运作的呢?首先,它会有两组堆栈(stack),分别用来存放Undo与Redo的操作,而每一个操作都会以NSInvocation这个类来封装。很明显地,在这个类里面必须记录这个操作所使用的选择器(selector)、这个选择器的信息接收者以及所有的参数。
这里用一个例子来说明这一对堆栈的运作情况。假设有一个文本框textView,用户将原先的文字“ABC”修改为“DEF”,则这个修改的操作将会被以“反向”操作方式推入堆栈,也就是说将用户输入的“DEF”的文字还原为“ABC”的操作,如图6.20所示。
当我们按下“Undo”后,textView的文字会改回“ABC”,而这时候NSInvocation A会推出Undo堆栈,此时,Redo堆栈则是一个与NSInvocation A相反的操作NSInvocation B。
从上面的说明来看,当执行了一项操作时,我们必须要提供给NSUndoManager一个反向的操作。例如:当我们新增了一笔数据时,其反向操作就是将这笔数据删除。看起来,我们的程序代码似乎得多写不少东西。好在大部分的图形化控件都有其各自的NSUndoManager,所以要在原有的程序上加上Undo与Redo的功能其实是很简单的。接下来,我们可以把在第5章写的UITextViewDemo这个例子稍微改写一下,如下所示:
-(void) undoInput:(id) sender { self.navigationItem.leftBarButtonItem = nil; [[content undoManager] undo]; // [content setText:prevText]; }上面程序中批注的部分就是我们尚未介绍NSUndoManager之前的做法,但是有了NSUndoManager之后,只需要简简单单的一行就可以实现Undo的功能了。当然,也可以再加上Redo的功能,只要调用[[content undoManager] redo]就可以了。
虽然部分图形化控件都提供这种功能,但是,我们是否也可以将Undo-Redo机制应用在其他非图形化控件上呢?其实也是可以的,不过我们需要自己产生一个NSUndoManager,并建立相对应的Undo与Redo机制。例如:
NSUndoManager *manager = [NSUndoManager new]; 还可以通过 prepareWithInvocationTarget:来建立您的Undo的操作。 [[manager prepareWithInvocationTarget:self] myUndoAction]; 有了Invocation之后就可以调用Undo与Redo方法来进行Undo与Redo。应用范例:破裂的手机现在我们要应用Undo-Redo机制以及摇晃事件的检测来制作一个有趣的应用程序。这个应用程序一开始会让界面呈现破碎的样子,当然这并非是真的,而是我们制作出来的效果。但是当其他人看到这个界面的时候,肯定会很吃惊。这时候,只要摇晃一下手机就会恢复原状,再摇晃一次界面就会变成破碎的模样。
学习重点:
Undo-Redo机制的使用摇晃事件的应用动画的使用建立一个Single View的项目,并命名为“BrokeniPhone”。
在建立项目过程中,请记得勾选“Use Storyboard”以及“Use Automatic Reference Counting”选项。
在项目中加入两张手机的界面。
在设计界面中加入Image View控件,如图6.21所示。
打开“MainStorybard.storyboard”并从控件库中加入一个Image View控件到界面中。这时候,请指定该控件的图形为破碎后的手机界面。
选中界面中的手机背景,然后打开编辑器的辅助模式,按住“Control”键后拉到ViewController.h以建立Outlet。完成后,我们先在ViewController.h中定义一个NSUndoManager的变量以及restoreScreen和breakScreen这两个方法,代码如下:
#import <UIKit/UIKit.h> @interface ViewController : UIViewController{ @private NSUndoManager *undoManager; } @property (strong, nonatomic) IBOutlet UIImageView *screen; -(void) restoreScreen; -(void) breakScreen; @end打开ViewController.m,并编辑如下:
处理晃动事件的第一步就是让目前的ViewController可以成为First Responder。
// 让ViewController可以成为First Responder - (BOOL) canBecomeFirstResponder { return YES; } 接着要让ViewController变成First Responder,而在界面消失后让ViewController变成非First Responder。 - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; // 让ViewController变成First Responder [self becomeFirstResponder]; } - (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; // 让ViewController变成非First Responder [self resignFirstResponder]; } 最后,加入摇晃操作的检测事件处理就好了。 - (void) motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event { if (motion==UIEventSubtypeMotionShake) { if ([undoManager canUndo]) { [undoManager undo]; }else{ [undoManager redo]; } } }由于我们要让undoManager在Undo与Redo间不断切换,因此,我们可以通过canUndo这个方法来判断在Undo堆栈里面是否有Invocation。
这样就完成了晃动事件的处理。
现在开始初始化undoManager。由于刚开始的界面是破碎的模样,因此,我们在Undo的堆栈里面要建立一个恢复原状的Invocation。
- (void)viewDidLoad { [super viewDidLoad]; undoManager = [NSUndoManager new]; [[undoManager prepareWithInvocationTarget:self] restoreScreen]; } 建立好Invocation后,undoManager就可以进行Undo了。 7.jpg 建立界面破碎以及恢复界面的操作。 - (void) breakScreen { [UIView transitionWithView:screen duration:0.5f options:UIViewAnimationOptionTransitionCrossDissolve animations: ^(void){ screen.image = [UIImage imageNamed:@"broken.png"]; } completion:nil]; [[undoManager prepareWithInvocationTarget:self] restoreScreen]; } - (void) restoreScreen { [UIView transitionWithView:screen duration:0.5f options:UIViewAnimationOptionTransitionCrossDissolve animations:^(void){ screen.image = [UIImage imageNamed:@"normal.png"]; } completion:nil]; [[undoManager prepareWithInvocationTarget:self] breakScreen]; }这两个方法的操作刚好相反,包括加载的图形以及堆栈内的Invocation。我们在每一个操作内都要去建立相反的操作,这样在堆栈内才有相反的操作可以还原回去。现在,您可以拿着这个应用程序去吓唬您的朋友,相信他们应该会吓一跳吧。不过这样的应用程序并不适合上架,被Apple拒绝的几率也十分高,读者们可以好好思考一下其中的原因。
相关资源:敏捷开发V1.0.pptx