首页 » iOS编程(第4版) » iOS编程(第4版)全文在线阅读

《iOS编程(第4版)》17.4 更多的模态视图控制器

关灯直达底部

本节将指导读者更新Homepwner,使其能够在添加BNRItem对象时以模态的形式显示一个BNRDetailViewController对象,供用户创建新的BNRItem对象(见图17-3)。当用户选中某个已存在的BNRItem对象时,Homepwner的处理不变,还是会将该对象压入UINavigationController对象的栈。

图17-3 添加BNRItem对象

为了实现BNRDetailViewController的两种使用模式,需要创建新的指定初始化方法initForNewItem:。initForNewItem:需要根据传入的使用模式(创建新的BNRItem对象,或者显示已存的BNRItem对象)设置相应的界面。

在BNRDetailViewController.h中声明initForNewItem:,代码如下:

- (instancetype)initForNewItem:(BOOL)isNew;

@property (nonatomic, strong) BNRItem *item;

当Homepwner使用BNRDetailViewController对象创建新的BNRItem对象时,需要在相应的UINavigationBar对象两端分别显示Done(完成)按钮和Cancel(取消)按钮。在BNRDetailViewController.m中实现initForNewItem:,代码如下:

- (instancetype)initForNewItem:(BOOL)isNew

{

self = [super initWithNibName:nil bundle:nil];

if (self) {

if (isNew) {

UIBarButtonItem *doneItem = [[UIBarButtonItem alloc]

initWithBarButtonSystemItem:UIBarButtonSystemItemDone

target:self

action:@selector(save:)];

self.navigationItem.rightBarButtonItem = doneItem;

UIBarButtonItem *cancelItem = [[UIBarButtonItem alloc]

initWithBarButtonSystemItem:UIBarButtonSystemItemCancel

target:self

action:@selector(cancel:)];

self.navigationItem.leftBarButtonItem = cancelItem;

}

}

return self;

}

在之前的代码中,当某个类要弃用父类的指定初始化方法并改用新的指定初始化方法时,都会覆盖父类的指定初始化方法并调用新的那个。本节要修改的BNRDetail- ViewController类不同,为了禁止使用父类的指定初始化方法,覆盖后的initWithNibName:bundle:方法不仅不应执行任何代码,而且要抛出异常,提示“禁止使用该初始化方法”。

在BNRDetailViewController.m中覆盖父类的指定初始化方法,代码如下:

- (instancetype)initWithNibName:(NSString *)nibNameOrNil

bundle:(NSBundle *)nibBundleOrNil

{

@throw [NSException exceptionWithName:@“Wrong initializer”

reason:@“Use initForNewItem:”

userInfo:nil];

return nil;

}

覆盖后的initWithNibName:bundle:会创建并抛出一个NSException对象。新创建的NSException对象包含名称(name属性)和原因(reason属性)。抛出异常后,应用会终止运行,并向控制台输出相应的错误信息。

为了测试initWithNibName:bundle:是否会抛出异常,需要先找到调用该方法的代码,即BNRItemsViewController的tableView:didSelectRowAtIndexPath:。在tableView: didSelectRowAtIndexPath:中,BNRItemsViewController对象会创建一个BNRDetailViewController对象,然后向新创建的对象发送init消息,init方法又会调用initWithNibName:bundle:。因此,当用户选中UITableView对象中的某一行时,就会导致应用抛出名称为Wrong initializer(调用错误的初始化方法)的异常。

构建并运行应用,(Xcode会警告找不到save:和cancel:方法,可以忽略这些警告。)选中UITableView对象中的某一行,Homepwner会终止运行并向控制台输出相应的异常信息,其中包括异常的名称和原因。

为了修正这个错误,也为了能够正确地初始化BNRDetailViewController对象,下面要修改BNRItemsViewController.m中的tableView:didSelectRowAtIndexPath:,使用新的指定初始化方法,代码如下:

- (void)tableView:(UITableView *)tableView

didSelectRowAtIndexPath:(NSIndexPath *)indexPath

{

BNRDetailViewController *detailViewController =

[[BNRDetailViewController alloc] init];

BNRDetailViewController *detailViewController =

[[BNRDetailViewController alloc] initForNewItem:NO];

NSArray *items = [[BNRItemStore sharedStore] allItems];

再次构建并运行应用,选中UITableView对象中的某一行,Homepwner应该不会再崩溃。

下面要添加新的代码,使Homepwner能够在用户添加新的BNRItem对象时显示BNRDetailViewController对象。

修改BNRItemsViewController.m中的addNewItem:方法,先创建一个新的BNRDetailViewController对象,然后创建一个新的UINavigationController对象,并将之前创建的BNRDetailViewController对象设置为该对象的根视图控制器,最后用模态形式显示该对象,代码如下:

- (IBAction)addNewItem:(id)sender

{

BNRItem *newItem = [[BNRItemStore sharedStore] createItem];

NSInteger lastRow =

[[[BNRItemStore sharedStore] allItems] indexOfObject:newItem];

NSIndexPath *indexPath = [NSIndexPath indexPathForRow:lastRow inSection:0];

[self.tableView insertRowsAtIndexPaths:@[indexPath]

withRowAnimation:UITableViewRowAnimationTop];

BNRDetailViewController *detailViewController =

[[BNRDetailViewController alloc] initForNewItem:YES];

detailViewController.item = newItem;

UINavigationController *navController = [[UINavigationController alloc]

initWithRootViewController:detailViewController];

[self presentViewController:navController animated:YES completion:nil];

}

构建并运行应用,按下+按钮,BNRDetailViewController对象的视图应该会从窗口底部滑入,相应的UINavigationBar对象会显示Done按钮和Cancel按钮(因为没有实现相应的动作方法,所以按下这两个按钮都会导致应用抛出异常)。

注意,即使BNRDetailViewController对象不会再导航到其他视图控制器,也需要将其先压入导航控制器栈中再推入,这样就不用为了显示标题以及放置Done按钮和Cancel按钮而单独添加一个导航栏(UINavigationBar对象)。

关闭模态视图控制器

要关闭某个以模态形式显示的视图控制器,必须向负责显示该对象的视图控制器发送dismissViewControllerAnimated:completion:。本章之前就是这样处理UIImagePicker- Controller对象的:BNRDetailViewController对象会负责以模态形式显示一个UIImagePickerController对象,当需要关闭UIImagePickerController对象时,也会由BNRDetailViewController对象负责关闭该对象。

添加BNRItem对象时的情况有些特别。添加该对象时,Homepwner会以模态的形式显示BNRDetailViewController对象。此外,相应的UINavigationBar对象会显示Done和Cancel两个按钮,当用户按下这两个按钮时,Homepwner会关闭BNRDetailViewController对象。这里的问题是,这两个按钮会将动作消息发送给BNRDetailViewController对象,但是负责关闭这个对象的是BNRItemsViewController对象。因此,BNRItemsView- Controller对象要通过某种途径得到以模态形式显示自己的那个视图控制器的指针,然后向该对象发送dismissViewControllerAnimated:completion:消息,从而关闭自己。

UIViewController对象有一个名为presentingViewController的属性,当某个UIViewController对象以模态形式显示时,该属性会指向显示该对象的那个UIViewController对象。因此,BNRDetailViewController对象可以通过presentingView- Controller属性得到所需的指针,然后向该指针指向的视图控制器发送dismissView- ControllerAnimated:completion:消息。在BNRDetailViewController.m中实现Done按钮的动作方法,代码如下:

- (void)save:(id)sender

{

[self.presentingViewController dismissViewControllerAnimated:YES

completion:nil];

}

Cancel按钮的动作方法会复杂一些。在BNRItemsViewController对象的视图中,当用户按下+按钮后,Homepwner会创建一个新的BNRItem对象,然后将该对象加入BNRItemStore对象。接着,Homepwner才会创建并显示BNRDetailViewController对象,供用户设置新创建的BNRItem对象。所以,当用户按下Cancel按钮时,Homepwner要从BNRItemStore对象移除之前创建的BNRItem对象。先在BNRDetailViewController.m顶部导入BNRItemStore.h,代码如下:

#import “BNRDetailViewController.h”

#import “BNRItem.h”

#import “BNRImageStore.h”

#import “BNRItemStore.h”

@implementation BNRDetailViewController

然后在BNRDetailViewController.m中实现Cancel按钮的动作方法,代码如下:

- (void)cancel:(id)sender

{

// 如果用户按下了Cancel按钮,就从BNRItemStore对象移除新创建的BNRItem对象

[[BNRItemStore sharedStore] removeItem:self.item];

[self.presentingViewController dismissViewControllerAnimated:YES

completion:nil];

}

构建并运行应用,添加新的BNRItem对象并按下Cancel按钮。BNRDetailViewController对象的视图应该会滑出窗口,且UITableView对象也不会加入新行。再添加一个BNRItem对象并按下Done按钮。BNRDetailViewController视图一样会滑出窗口,但是UITableView对象这次会加入新行。

最后还有一点需要向读者说明。前文提到过,负责以模态形式显示BNRDetailViewController对象的是BNRItemsViewController对象。其实这是为了方便读者理解而给出的简化的不准确的解释,实际的关系更复杂。BNRDetailViewController对象的presentingViewController属性指向的并不是BNRItemsViewController对象,而是包含该对象的UINavigation- Controller对象。这是由Cocoa Touch的内部实现机制决定的。这也是为什么当以模态形式显示BNRDetailViewController对象时,该对象的视图会遮住UINavigationBar对象。

如果读者要实现的只是关闭模态视图控制器,就不用关心presentingViewController属性指向的究竟是哪个对象。只需要向该属性指向的对象发送dismissViewControllerAnimated: completion:消息即可。本章会在结尾处详细解释与此有关的视图控制器关系。

视图控制器的模态样式

在iPhone或iPod touch中以模态形式显示视图控制器时,视图控制器的视图会占据整个窗口。对iPhone系设备,这是默认的样式,也是唯一的选择。对iPad则有两个额外的选项:表单样式(form sheet)和页单样式(page sheet)。修改视图控制器的modalPresentationStyle属性,可以改变其在模态形式下的外观。该属性的类型是UIModalPresentationStyle,其值可以是UIModalPresentationFormSheet或UIModalPresentationPageSheet两个常量中的一个。

在表单样式下,模态视图控制器的视图会出现在iPad屏幕中部的矩形区域,并且其下层的视图(即parentViewController的视图)会变暗(见图17-4)。

图17-4 表单样式示例

在页单样式下,当设备处于竖排方向时,视图的显示效果和其在全屏样式下的相同。当设备处于横排方向时,视图的宽度和其在竖排方向时的相同。此外,在视图的左右两侧,位于其下层的视图(即parentViewController的视图)会变暗。

更新BNRItemsViewController.m中的addNewItem:,修改将要以模态形式显示的UINavigationController对象的外观样式,代码如下:

UINavigationController *navController = [[UINavigationController alloc]

initWithRootViewController:detailViewController];

navController.modalPresentationStyle = UIModalPresentationFormSheet;

[self presentViewController:navController animated:YES completion:nil];

值得注意的是,这段代码修改的是UINavigationController对象的modalPresentation- Style,而不是BNRDetailViewController对象的。这是因为以模态形式显示的视图控制器是UINavigationController对象。

针对iPad模拟器或iPad构建并运行应用。按下+按钮,添加新的BNRItem对象,相应的视图控制器应该会滑入窗口。填入若干BNRItem对象的详细信息并按下Done按钮。UITableView对象会再次出现,但是并没有显示新的BNRItem对象。为什么?

修改模态样式前,UINavigationController对象会占据整个屏幕,并彻底遮住BNRItemsViewController对象的视图,即导致该视图“消失”。关闭模态视图控制器后,BNRItemsViewController对象的视图会再次出现,因此该对象会收到viewWillAppear:消息和viewDidAppear:消息,从而有机会重新刷新UITableView对象,使其显示的内容和BNRItemStore对象所保存的数据保持一致。

修改模态样式后,UINavigationController对象不会占据整个屏幕,BNRItemsViewController对象的视图也不会消失。关闭模态视图控制器后,BNRItemsViewController对象不会收到viewWillAppear:消息和viewDidAppear:消息,因此也没有机会刷新UITableView对象。

为了解决上述问题,必须另找机会来刷新UITableView对象。先准备要加入的代码,在BNRItemsViewController对象的某个方法中刷新UITableView对象,只需编写一行简单的代码,如下:

[self.tableview reloadData];

为了能够在模态视图控制器被关闭后立刻执行这行代码,可以使用视图控制器的dismissViewControllerAnimated:completion:方法。

类型为Block对象的completion实参

前文中的代码在调用dismissViewControllerAnimated:completion:方法和presentViewController:animated:completion:方法时,传入的最后一个实参都是nil。以dismissViewControllerAnimated:completion:为例,它的最后一个实参声明如下。

- (void)dismissViewControllerAnimated:(BOOL)flag

completion:(void (^)(void))completion;

completion实参的类型是“Block对象”。通过dismissViewControllerAnimated: completion:的这个实参,可以解决之前提到的问题。在使用该实参前,要先向读者介绍Block对象。Block对象的概念和语法并不容易掌握,所以本节只进行简单介绍。后续章节在涉及Block对象时,会穿插讲解Block对象在各类场景下的具体用法。

Block对象封装了一段用于延时执行的代码,因此,可以将刷新UITableView对象的那行代码放入一个Block对象,然后将指向该对象的指针作为实参传入dismissViewController- Animated:completion:。当相关的视图控制器被关闭后,Homepwner就会执行Block对象中的这段代码。

在BNRDetailViewController.h中为BNRDetailViewController添加一个属性,用于保存指向Block对象的指针。

@property (nonatomic, copy) void (^dismissBlock)(void);

这行代码的作用是声明BNRDetailViewController拥有一个名为dismissBlock的属性,该属性是一个指向Block对象的变量。类似C函数,Block对象也有返回值和一组实参,必须在声明Block对象时一并列出。以dismissBlock属性所指向的Block对象为例,其返回值是void,并且没有任何实参。

为了访问需要刷新的UITableView对象,必须在BNRItemsViewController的实例方法中创建相应的Block对象。这是因为只有BNRItemsViewController对象才能够通过tableView属性访问其UITableView对象。

修改BNRItemsViewController.m中的addNewItem:,创建一个负责刷新UITableView对象的Block对象,并将该对象赋给BNRDetailViewController对象,代码如下:

- (IBAction)addNewItem:(id)sender

{

// 创建BNRItem对象,然后将新创建的对象加入BNRItemStore对象

BNRItem *newItem = [[BNRItemStore sharedStore] createItem];

BNRDetailViewController *detailViewController =

[[BNRDetailViewController alloc] initForNewItem:YES];

detailViewController.item = newItem;

detailViewController.dismissBlock = ^{

[self.tableView reloadData];

};

UINavigationController *navController = [[UINavigationController alloc]

initWithRootViewController:detailViewController];

当用户按下+按钮添加一个BNRItem对象时,BNRItemsViewController对象会创建一个Block对象并将其指针赋给BNRDetailViewController对象的dismissBlock属性。该Block对象所包含的代码会刷新BNRItemsViewController对象的UITableView对象。

当要关闭以模态形式显示的BNRDetailViewController对象时,可以将dismissBlock属性所指向的Block对象传给dismissViewControllerAnimated: completion:。修改BNRDetailViewController.m中的save:和cancel:,在调用dismissViewControllerAnimated:completion:时将dimissBlock属性作为最后一个实参传入,代码如下:

- (IBAction)save:(id)sender

{

[self.presentingViewController dismissViewControllerAnimated:YES

completion:nil];

[self.presentingViewController dismissViewControllerAnimated:YES

completion:self.dismissBlock];

}

- (IBAction)cancel:(id)sender

{

[[BNRItemStore sharedStore] removeItem:self.item];

[self.presentingViewController dismissViewControllerAnimated:YES

completion:nil];

[self.presentingViewController dismissViewControllerAnimated:YES

completion:self.dismissBlock];

}

构建并运行应用。按下+按钮,添加一个BNRItem对象,然后按下Done按钮。UITableView对象应该会显示新创建的BNRItem对象。

读者目前不用太在意Block对象的语法和使用方法,从第19章起会详细介绍Block对象。

以模态形式显示视图控制器时的动画效果

前文介绍了如何修改视图控制器的模态样式。当应用以模态形式显示某个视图控制器时,会附带特定的动画效果,这个动画效果也是可以修改的。类似模态样式,视图控制器有一个名为modalTransitionStyle的属性,该属性的类型是UIModalTransitionStyle,其值必须是相关的预定义常量之一。如果使用默认的动画效果,则视图控制器的视图会从屏幕底部滑入。其他可以使用的动画效果有淡入(fade in)、翻转(flip in)和模拟书页卷角(page curl)。

以下是这些动画效果所对应的常量:

UIModalTransitionStyleCoverVertical从底部滑入

UIModalTransitionStyleCrossDissolve淡入

UIModalTransitionStyleFlipHorizontal以3D效果翻转

UIModalTransitionStylePartialCurl模拟书页卷角