前面已经为Homepwner加入了一个UINavigationController对象,并创建了BNRItemsViewController和BNRDetailViewController这两个UIViewController子类。下面将这些对象组合在一起以实现其功能:当用户点击BNRItemsViewController对象的表格视图中的某一行时,Homepwner会创建一个BNRDetailViewController对象,然后将该对象的视图推入窗口,显示当前选中的BNRItem对象的各项属性。
将视图控制器压入栈
要完成上述任务,就必须创建BNRDetailViewController对象。问题是该在何处创建该对象?本章之前的应用都会在application:didFinishLaunchingWithOptions:中创建所有的视图控制器。例如,第6章在application:didFinishLaunchingWithOptions:中创建了两个视图控制器并将它们加入了UITabBarController对象的viewControllers数组。
但是,在使用UINavigationController时,不能简单地将所有可能用到的视图控制器都压入UINavigationController对象的栈。该对象的viewControllers数组是动态的:一开始只有一个根视图控制器,应用需要根据情况推入新的视图控制器。因此,Homepwner需要某个对象(除UINavigationController对象外)来负责创建BNRDetailViewController对象,并将新创建的对象压入UINavigationController对象的栈。
这个负责创建BNRDetailViewController对象的对象需要满足两个条件:首先,该对象要知道在什么时候将BNRDetailViewController对象压入栈。其次,该对象需要拥有一个指向UINavigationController对象的指针,以便向UINavigationController对象发送pushViewController:animated:消息。
BNRItemsViewController对象满足上述两个条件。首先,因为该对象是UITableView对象的委托对象,所以,当用户点击UITableView对象的某个表格行时,BNRItemsViewController对象会收到tableView:didSelectRowAtIndexPath:消息。其次,凡是加入了某个UINavigationController对象的栈的视图控制器,都可以向自己发送navigationController消息,以得到指向该对象的指针。因为应用会将BNRItemsViewController对象设置为UINavigationController对象的根视图控制器,所以BNRItemsViewController对象会一直留在UINavigationController对象的栈中,从而使BNRItemsViewController对象总能得到指向相应UINavigationController对象的指针。
因此,应该由BNRItemsViewController对象负责创建BNRDetailViewController对象并将其加入UINavigationController对象的栈。在BNRItemsViewController.h顶部导入BNRDetailViewController的头文件。
#import “BNRDetailViewController.h”
@interface BNRItemsViewController : UITableViewController
当用户点击UITableView对象中的某个表格行时,该对象会向其委托对象发送tableView:didSelectRowAtIndexPath:消息并传入选中的行的索引信息。在BNRItemsViewController.m中实现tableView:didSelectRowAtIndexPath:,创建BNRDetailViewController对象,然后将新创建的对象压入UINavigationController对象的栈,代码如下:
@implementation BNRItemsViewController
- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
BNRDetailViewController *detailViewController =
[[BNRDetailViewController alloc] init];
// 将新创建的BNRDetailViewController对象压入UINavigationController对象的栈
[self.navigationController pushViewController:detailViewController
animated:YES];
}
构建并运行应用,添加一新行并选中之。Homepwner应该会以动画的形式,将BNRDetailViewController对象的视图从屏幕右侧推入。此外,位于窗口顶部的UINavigationBar对象会在其左端显示一个标题为“Back(返回)”的按钮。点击这个按钮,UINavigationController对象会弹出位于栈顶的BNRDetailViewController对象,退回至BNRItemsViewController对象。
因为UINavigationController对象的栈是一个数组,会拥有其包含的所有视图控制器,所以当应用执行完tableView:didSelectRowAtIndexPath:方法后,该对象将成为新创建的BNRDetailViewController对象的唯一拥有者。一旦UINavigationController对象将某个BNRDetailViewController对象弹出栈,弹出的这个对象就会立刻被释放。当用户再次选中某个表格行时,应用会创建新的BNRDetailViewController对象。
使用UINavigationController对象时,经常会由当前处于栈顶的视图控制器来负责压入另一个视图控制器,这是常见的使用模式。也就是说,UINavigationController对象的根视图控制器会负责创建并压入下一个视图控制器,而下一个视图控制器会负责创建并压入再下一个视图控制器,依此类推。有些应用的视图控制器可以根据用户的输入创建并压入不同类型的视图控制器。以iOS自带的照片(Photos)应用为例,照片应用会根据用户选中的媒体类型将视频视图控制器或图片视图控制器压入UINavigationController对象的栈。
只能用于iPad的UISplitViewController类,使用的是另一种模式。iPad拥有更大的屏幕,可以同时显示垂直界面中的两个视图控制器,从而不需要将后者压入栈。第22章会对UISplitViewController作更多的介绍。
视图控制器之间的数据传递
下面为UITextField对象设置显示内容。为此,需要通过某种途径将选中的BNRItem对象从BNRItemsViewController对象传入BNRDetailViewController对象。
首先为BNRDetailViewController添加一个属性,用来保存指定的BNRItem对象。当用户点击UITableView对象中的某个表格行时,BNRItemsViewController对象应该将选中的BNRItem对象传给即将被压入栈的BNRDetailViewController对象。得到BNRItem对象后,BNRDetailViewController对象就可以针对相应的BNRItem属性设置所有的UITextField对象。当用户修改了某个UITextField对象后,BNRDetailViewController对象也会将新的值赋给相应的BNRItem对象属性。
在BNRDetailViewController.h中,先在文件顶部用@class指令来前置声明BNRItem,然后为BNRDetailViewController类添加一个属性,代码如下:
#import <UIKit/UIKit.h>
@class BNRItem;
@interface BNRDetailViewController : UIViewController
@property (nonatomic, strong) BNRItem *item;
@end
在BNRDetailViewController.m中,导入BNRItem的头文件:
#import “BNRItem.h”
当应用要显示BNRDetailViewController对象的视图时,该对象需要根据item的各个属性设置其视图的子视图。在BNRDetailViewController.m中覆盖viewWillAppear:,将BNRItem对象的各个属性赋给相应的UITextField对象,代码如下:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
BNRItem *item = self.item;
self.nameField.text = item.itemName;
self.serialNumberField.text = item.serialNumber;
self.valueField.text =
[NSString stringWithFormat:@“%d”, item.valueInDollars];
// 创建NSDateFormatter对象,用于将NSDate对象转换成简单的日期字符串
static NSDateFormatter *dateFormatter = nil;
if (!dateFormatter) {
dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.dateStyle = NSDateFormatterMediumStyle;
dateFormatter.timeStyle = NSDateFormatterNoStyle;
}
// 将转换后得到的日期字符串设置为dateLabel的标题
self.dateLabel.text = [dateFormatter stringFromDate:item.dateCreated];
}
接下来在BNRItemsViewController.m的tableView:didSelectRowAtIndexPath:方法中添加以下代码,以便在BNRDetailViewController对象收到viewWillAppear:消息前将其item属性设置为相应的BNRItem对象。
- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
BNRDetailViewController *detailViewController =
[[BNRDetailViewController alloc] init];
NSArray *items = [[BNRItemStore sharedStore] allItems];
BNRItem *selectedItem = items[indexPath.row];
// 将选中的BNRItem对象赋给DetailViewController对象
detailViewController.item = selectedItem;
[self.navigationController pushViewController:detailViewController
animated:YES];
}
当要在多个视图控制器之间传递数据时,很多刚接触iOS开发的初学者可能会无从下手。Homepwner使用了一种简单高效的解决方案:由根视图控制器保存所有的数据,然后将数据的子集传给下一个视图控制器。
构建并运行应用,添加一新的表格行并选中。Homepwner会显示BNRDetailViewController对象的视图,其中包含选中的BNRItem对象的详细信息。虽然用户可以编辑这些信息,但是当屏幕返回至BNRItemsViewController对象后,UITableView对象不会根据用户做出的修改来更新显示内容。要解决这个问题,需要编写代码,根据用户的输入来修改BNRItem对象的相应属性。
视图的显示和消失
当UINavigationController对象切换视图时,其包含的两个UIViewController对象会分别收到viewWillDisappear:消息和viewWillAppear:消息。即将出栈的UIViewController对象会收到viewWillDisappear:消息,即将入栈的UIViewController对象会收到viewWillAppear:消息。
当某个BNRDetailViewController对象退出栈时,应该将各个UITextField对象的值赋给BNRItem对象的相应属性。覆盖viewWillDisappear:和viewWillAppear:时,必须先调用父类的实现,以完成必要的工作。在BNRDetailViewController.m中实现viewWillDisappear:,代码如下:
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
// 取消当前的第一响应对象
[self.view endEditing:YES];
// 将修改“保存”至BNRItem对象
BNRItem *item = self.item;
item.itemName = self.nameField.text;
item.serialNumber = self.serialNumberField.text;
item.valueInDollars = [self.valueField.text intValue];
}
这段代码使用了UIView的endEditing:方法。当某个视图收到endEditing:消息时,如果该视图(或者其下的任何子视图)是当前的第一响应对象,就会取消自己的第一响应对象状态,而且虚拟键盘也会关闭(传入的参数代表是否需要强制取消第一响应对象状态。某些第一响应对象可能会拒绝退出状态,传入YES可以强制其退出)。
更新代码后,当用户点击UINavigationBar对象上的Back(返回)按钮时,BNRDetailViewController对象就会更新相应的BNRItem对象。当BNRItemsView- Controller对象的视图再次出现在屏幕上时,它就会收到viewWillAppear:消息。这时应该刷新UITableView对象,使用户能够立刻看到更新后的数据。在BNRItemsViewController.m中覆盖viewWillAppear:,代码如下:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.tableView reloadData];
}
构建并运行应用。现在应该可以在BNRItemsViewController对象和BNRDetail- ViewController对象之间来回切换并修改数据,UITableView对象会及时刷新,以显示更新后的BNRItem对象。