本节将创建第二个视图控制器——BNRReminderViewController。该视图控制器的界面用于让用户选择催眠时间(见图6-6),然后在该时间提醒用户。请注意,即使HypnoNerd此时不再运行,用户也应该能收到提醒。
图6-6 BNRReminderViewCobtroller
使用Xcode创建一个新的Objective-C类(键盘快捷键是Command-N),选择NSObject作为父类,并将其命名为BNRReminderViewController。
在BNRReminderViewController.h中,将BNRReminderViewController的父类改为UIViewController,代码如下:
@interface BNRReminderViewController : NSObject
@interface BNRReminderViewController : UIViewController
BNRReminderViewController的view是一个全屏幕尺寸的UIView对象,并包含两个子视图:一个UIDatePicker对象和一个UIButton对象(见图6-7)。
图6-7 BNRReminderViewController的视图层次结构
因此,需要为BNRReminderViewController添加一个datePicker属性,指向一个UIDatePicker对象。同时,需要将BNRReminderViewController设置为UIButton对象的目标,还需要编写一个addReminder:方法并设置为UIButton对象的动作。
BNRReminderViewController对象的view会有两个子视图。当某个视图控制器的view拥有子视图时,使用Interface Builder创建视图层次结构会方便很多。
在Interface Builder中创建视图
首先打开BNRReminderViewController.m,添加一个类扩展,声明一个datePicker属性,然后创建一个addReminder:方法,向控制台输出datePicker的日期。
#import /"BNRReminderViewController.h/"
@interface BNRReminderViewController
@property (nonatomic, weak) IBOutlet UIDatePicker *datePicker;
@end
@implementation BNRReminderViewController
- (IBAction)addReminder:(id)sender
{
NSDate *date = self.datePicker.date;
NSLog(@/"Setting a reminder for %@/", date);
}
@end
第1章中介绍过,IBOutlet和IBAction关键字告诉Xcode,这些属性或方法之后会在Interface Builder中关联。
接下来需要创建一个XIB文件,从File菜单中选择New→File…,然后选中iOS部分的User Interface,再选中Empty(见图6-8),最后单击Next按钮。
图6-8 创建空的XIB文件
在新出现的面板中,选择Device Family下拉菜单中的iPhone,单击Next按钮。
将新文件命名为BNRReminderViewController.xib并保存。(请读者务必按照本书给出的文件名创建文件。书中很多文件名都是根据iOS SDK的各种约定取的,以便代码能够正常工作。)
选中项目导航面板中的BNRReminderViewController.xib,该文件会在Interface Builder中打开。
创建视图对象
从对象库面板(位于Xcode右下方)拖曳一个UIView对象至画布。默认情况下,该对象的大小和屏幕大小相同,不需要手动调整其大小。
然后再拖曳一个UIButton对象和一个UIDatePicker对象至UIView对象,并设置其大小和位置,再双击UIButton对象修改按钮标题,如图6-9所示。
图6-9 BNRReminderViewController的XIB文件
在画布左边的大纲视图中可以看见已经创建好的视图层次结构:View是根视图,其中包含两个子视图,分别是Picker和Button(见图6-10)。
图6-10 BNRReminderViewController.xib中的视图层次结构
加载NIB文件
当视图控制器从NIB文件中创建视图层次结构时,不需要覆盖loadView方法,默认的loadView方法会自动处理NIB文件中包含的视图层次结构。
接下来在UIViewController的指定初始化方法中为BNRReminderViewController设置需要加载的NIB文件:
- (instancetype)initWithNibName:(NSString *)nibName
bundle:(NSBundle *)nibBundle;
该方法包含两个参数,分别用于指定NIB文件的文件名及其所在的程序包。
在BNRAppDelegate.m文件顶部导入BNRReminderViewController.h,然后创建一个BNRReminderViewController对象,再将其设置为应用窗口的根视图控制器,代码如下:
#import /"BNRReminderViewController.h/"
@implementation BNRAppDelegate
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:
[[UIScreen mainScreen] bounds]];
// 在这里添加应用启动后的初始化代码
// 这段代码不会使用hvc,所以Xcode会显示一处相应的警告,先忽略
BNRHypnosisViewController *hvc = [[BNRHypnosisViewController alloc] init];
// 获取指向NSBundle对象的指针,该NSBundle对象代表应用的主程序包
NSBundle *appBundle = [NSBundle mainBundle];
// 告诉初始化方法在appBundle中查找BNRReminderViewController.xib文件
BNRReminderViewController *rvc = [[BNRReminderViewController alloc]
initWithNibName:@/"BNRReminderViewController/"
bundle:appBundle];
self.window.rootViewController = rvc;
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
向NSBundle发送mainBundle消息可以得到应用的主程序包。主程序包对应于文件系统中项目的根目录,包含代码文件和资源文件(例如NIB文件和图片),初始化方法需要的BNRReminderViewController.xib文件也包含在主程序包中。
至目前为止,视图层次结构中的所有视图对象都已经创建和设置好了,视图控制器的初始化方法可以正确加载NIB文件了,视图控制器也成为UIWindow的根视图控制器并将视图层次结构加入到应用窗口中了。但是,如果现在构建并运行应用,则应用会崩溃,请注意控制台中输出的异常信息:
/'-[UIViewController _loadViewFromNibNamed:bundle:] loaded the
/"BNRReminderViewController/" nib but the view outlet was not set./'
当NIB文件被加载时,会创建文件中的视图对象,但是这些视图对象在应用运行时并没有与BNRReminderViewController关联起来,包括BNRReminderViewController的view属性。当该视图控制器需要将view添加到应用窗口时,view是nil,因此应用崩溃并输出了异常信息。
那么如何将XIB文件中创建的视图对象在运行时与视图控制器进行关联呢?这时需要使用File/'s Owner对象。
关联File/'Owner
File/'s Owner对象是一个占位符对象(placeholder),它是XIB文件特意留下的一个“空洞”。当某个视图控制器将XIB文件加载为NIB文件时,首先会创建XIB文件中的所有视图对象,然后会将自己填入相应的File/'s Owner空洞,并建立之前在Interface Builder中设置的关联(见图6-11)。
图6-11 NIB文件加载过程的时间轴
因此,如果要在运行时关联加载NIB文件的对象,可在XIB文件中关联File/'s Owner。首先需设置BNRReminderViewController.xib文件中的File/'s Owner是BNRReminderViewController。
重新打开BNRReminderViewController.xib,选中大纲视图中的File/'s Owner,单击位于检视面板区域上方的按钮,打开标识检视面板(identity inspector)。将Class文本框中的内容(File/'s Owner表示的类)改为BNRReminderViewController(见图6-12)。
图6-12 针对File/'s Owner的标识检视面板
接下来在XIB文件中添加所需的关联,首先是视图控制器的view属性,在大纲视图中按住Control并单击File/'s Owner,Xcode会显示关联面板,列出所有可用的关联,在插座变量(outlets)部分有一个view。将view插座变量与画布中的UIView对象关联起来。这样,当BNRReminderViewController对象载入该XIB文件时,其view属性就能自动指向画布中的UIView对象(见图6-13)。
图6-13 关联view插座变量
现在,BNRReminderViewController对象在运行时可以加载view了,构建并运行应用,BNRReminderViewController会加载XIB文件中创建的UIView对象,应用也不会再崩溃了。
使用同样的方法关联其余插座变量,首先将插座变量datePicker关联至UIDatePicker对象,然后将UIButton对象关联至File/'s Owner并选择弹出式菜单中的addReminder:,如图6-14所示。
图6-14 BNRReminderViewController.xib中的关联
构建并运行应用,选择一个时间,然后点击Remind Me按钮,并检查控制台输出的时间是否与选择的提醒时间相同。之后的章节中会使用本地通知功能完成addReminder:方法。
之前的代码中将BNRReminderViewController的datePicker插座变量声明为弱引用。将插座变量声明为弱引用是一种编程约定。当系统的可用内存偏少时,视图控制器会自动释放其视图并在之后需要显示时再创建。因此,视图控制器应该使用弱引用特性的插座变量指向view的子视图,以便在释放view时同时释放view的所有子视图,从而避免内存泄漏。