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拒绝的几率也十分高,读者们可以好好思考一下其中的原因。