
iPhone 3.0发布日快乐!(注:3.0早发了,只是现在才有空翻译这篇文章)
为庆祝iPhone 3.0发布,而且为我们能畅所欲言谈论iPhone3.0 SDK而不用担心违反NDA,我特此带来“iPhone 3.0编程新特性教程系列”的第一篇文章。
第一篇教程展示的是怎样为你的应用程序带来晃动undo/redo功能。
NSUndoManager
Undo/redo功能是由NSUndoManager类提供的。有关此类的文档在 这里, 工作原理是保存一系列已发送的消息,然后按顺序取消最后一个动作,倒数第二个动作,直到最初的状态。
让我们用一个例子来说明它是怎样工作的吧:我们的城市因为忧心来往于街道的卡车日益增多而决定进行一项普查。工人们使用一个iPhone程序来统计卡车交通状况。看见一辆卡车,就按一下按钮。哦,不小心将一辆斯巴鲁森林人误认为成了卡车?不要紧,晃动一下就可以取消了。
我们从一段已经正常工作但缺乏undo/redo功能的程序开始,为其添加这些功能。获取源代码:
Source/GitHub
源代码在 GitHub。 为下载一份你自己的拷贝,你需要克隆repository: (译者注:实际上你可以直接下载,不用这么麻烦)
- 打开终端切换到存放代码的目录下。
- 输入git clone git://github.com/dcgrigsby/TallyTrucks.git来克隆repository。
对repository我进行了两次提交(commit)操作 – 第一次的提交是不包括undo/redo功能的,第二次的包括undo/redo功能。如果你打算一步一步跟随我的教程,那么你需要回复到较早的提交。在源代码目录:
- 输入 git checkout 2d3a8136f43a1bba5183b1160c165aea24b705f2
目标
TallyTrucks是一个尽量简化了的程序。它的UI包括一个带有单个大钮的视图,此视图不但显示了符合条件的卡车数量而且提供了增加计数的机制。
看一下TallyTrucksViewController.m: 像所有Objective-C程序一样, 我们发送消息。发送addATruck消息增加计数,发送removeATruck消息以减小计数。
让我们用NSUndoManager术语来考虑。(从物理学的角度看:每一个动作都需要一个同等但相反的反应。)对用户按下按钮触发addATruck的动作,我们需要执行removeATruck来取消它。
首先将NSUndoManager加入我们的项目,然后实现上文描述的动作。
添加NSUndoManager
我们将NSUndoManager作为实例变量加到view controller类中。
更新TallyTrucksViewController.h。更改第七第九行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #import <UIKit/UIKit.h> @interface TallyTrucksViewController : UIViewController { IBOutlet UIButton *button; int i; NSUndoManager *undoManager; } @property (nonatomic,retain) NSUndoManager *undoManager; -(IBAction)buttonPressed; -(void)addATruck; -(void)removeATruck; -(void)updateTitle; @end |
TallyTrucksViewController.m中有两个变化:
首先,合成(synthesize).h文件中定义的属性。在 @implementation TallyTrucksViewController 语句后增加……
1 | @synthesize undoManager; |
然后,更新viewDidLoad和dealloc。改变5和11 行:
1 2 3 4 5 6 7 8 9 10 11 12 | - (void)viewDidLoad { [super viewDidLoad]; i = 0; undoManager = [[NSUndoManager alloc] init]; } - (void)dealloc { [undoManager release]; [super dealloc]; } |
同等但相反的动作
我们已经加入了NSUndoManager实例, 我们需要开始为各动作配对其同等但相反的undo动作。
Undo动作有两种方式:registerUndoWithTarget:selector:object: 和 prepareWithInvocationTarget。前者发送带有一个参数的消息,后者使用NSInvocation (用于对象间存储和转发消息),它可以有任意参数。我们的addATruck和removeATruck方法有任意参数,所以使用NSInvocation方法。
更新TallyTrucksViewController.m中addATruck和removeATruck。改变见第四和第十一行:
1 2 3 4 5 6 7 8 9 10 11 12 13 | -(void)addATruck { i += 1; [[undoManager prepareWithInvocationTarget:self] removeATruck]; [self updateTitle]; } -(void)removeATruck { i -= 1; [[undoManager prepareWithInvocationTarget:self] addATruck]; [self updateTitle]; } |
当用户增加一辆卡车,”减少一辆卡车“的操作就存储在undo栈的顶部。而减少一辆卡车的操作具有同样的模式,只不过结果相反。
Redo
前面我说过NSUndoManager保存了一个undo动作栈。更准确地说,实际上有两个栈:一个用于undo动作,另一个用于redo同样的动作。当你undo一些动作原先构成这些动作的消息被存放在redo栈中。Redo实际上是undo一个undo动作。幸运的是,作为程序员,我们不必关心这些细节,因为所有这些都是由
undo manager处理的。
Shake
既然已实现了 undo manager 功能,我们当然禁不住要运行测试一下。你一定失望了。undo manager是必须的,但还不是足够的。我们还需要将其连接到晃动检测机制。
3.0 SDK已在UIApplication类中加入了一个布尔属性applicationSupportsShakeToEdit。 我们需要在程序的delegate(代理)中设置它。
更新TallyTrucksAppDelegate.m中applicationDidFinishLaunching。修改第三行:
1 2 3 4 5 6 | - (void)applicationDidFinishLaunching:(UIApplication *)application { application.applicationSupportsShakeToEdit = YES; [window addSubview:viewController.view]; [window makeKeyAndVisible]; } |
要接收shake(晃动)事件,你必须是first responder(第一响应者)。我们必须修改view controller使其能够成为first responder而且实际成为first responder,最后,在视图消失时放弃first responder身份。
在TallyTrucksViewController.m中加入canBecomeFirstResponder, viewDidAppear:, 和 viewWillDisappear: 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | -(BOOL)canBecomeFirstResponder { return YES; } -(void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; [self becomeFirstResponder]; } - (void)viewWillDisappear:(BOOL)animated { [self resignFirstResponder]; [super viewWillDisappear:animated]; } |
现在程序拥有undo/redo功能了。重新生成一下你的项目再试下!(你很有可能是在模拟器上测试。你可以通过Hardware菜单中同名(shake)菜单项模拟晃动动作。)
更好的Undo标记
在现有情况下,undo是没有经过修饰的。它只是简单地称作”Undo“和”Redo“。我们要做最终修改,将其改为”Undo Add A Truck“和”Redo Add A Truck“。
更新TallyTrucksViewController.m中addATruck方法。修改见第五行:
1 2 3 4 5 6 7 | -(void)addATruck { i += 1; [[undoManager prepareWithInvocationTarget:self] removeATruck]; [undoManager setActionName:@"Add A Truck"]; [self updateTitle]; } |
结论
这里只讨论了基本概念。当你运用在你自己的项目中时,不要成为first responder过长时间。对于更复杂的应用,你可能需要考虑将setActionName调用放在if (![undoManager isUndoing])代码块中。



