视图控制器是 UIViewController 类或其子类对象。每个视图控制器都负责管理一个视图层次结构,包括创建视图层级结构中的视图并处理相关用户事件,以及将整个视图层次结构添加到应用窗口。
创建一个程序,并将上节 JXHypnosisView 类导入到工程中。
- 创建 UIViewController 子类
打开工程,创建一个 UIViewController 子类文件,并将其命名为 JXHypnosisViewController
- UIViewController 的 view 属性
JXHypnosisViewController 控制器从父类集成下来一个重要属性: @property (nonatomic,strong) UIView * view; . view 属性指向一个 UIView 对象。那么这个view就是这个视图层次结构的根视图,当程序将 view 作为子视图加入窗口时,也会加入 UIViewController 对象所管理的整个视图层次结构;
视图控制器不会再起被创建出来的那一刻马上创建并载入相应的视图。只有当应用需要将某个视图控制器的视图显示到屏幕上时,相应的视图控制器才会创建其视图。这种延迟加载视图的做法能提高内存的使用效率。
视图控制器可以通过两种方式创建视图层次结构。
1. 代码方式:覆盖 UIViewController 的 loadView 方法。
2. NIB 方式:使用 Interface Builder 创建一个 NIB 文件,然后加入所需的视图层次结构,最后视图控制器会在运行时加载由该 NIB 文件 文件编译而成的 XIB 文件。
通过代码方法创建视图
在 JXHypnosisViewController.m 顶部导入头文件 JXHypnosisView.h ,然后覆盖 loadView 方法,创建一个大小与屏幕相同的 JXHypnosisView 对象,并将其赋给视图控制器的 view 属性:
#import "JXHypnosisViewController.h" #import "JXHypnosisView.h" @interface JXHypnosisViewController () @end @implementation JXHypnosisViewController - (void)viewDidLoad { [super viewDidLoad]; } - (void)loadView { // 创建一个 JXHypnosisView 对象 JXHypnosisView * backgroundView = [[JXHypnosisView alloc] init]; // 将 JXHypnosisView 对象赋给视图控制器的view 属性 self.view = backgroundView; } @end
视图控制器刚被创建时,其 view 属性会被初始化为 nil 。之后,当应用需要将该视图控制器的视图显示到屏幕上时,如果 view 属性是 nil ,就会自动调用 loadView 方法。
接下来将 JXHypnosisViewController 对象是视图层次结构加入应用窗口,并将其显示在屏幕上。
设置根视图控制器
为了将视图控制器的视图层次结构加入应用窗口, UIWindow 对象提供了一个方法, setRootViewController: 。当程序将某个视图控制器设置为 UIWindow 对象的 rootViewController 时, UIWindow 对象会将该视图控制器的view作为子视图家兔窗口,此外,还会自动调整view的大小,将其设置为与窗口的大小相同。
首先在 AppDelegate.m 文件中导入需要的控制器,最后将其设置为 UIWindow 对象的根控制器
#import "AppDelegate.h" #import "JXHypnosisViewController.h" @interface AppDelegate () @end @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; // 在这里添加代码初始化 JXHypnosisViewController * hvc = [[JXHypnosisViewController alloc] init]; self.window.rootViewController = hvc; self.window.backgroundColor = [UIColor whiteColor]; [self.window makeKeyAndVisible]; return YES; }
rootViewController 的view需要在启动完毕后就显示,所以 UIWindow 对象会在设置完 rootViewController 后立刻加载 view。
- 另一个视图控制器
创建另外一个类,命名为 JXReminderViewController ,这个控制器用来让用户选择催眠时间,然后在该时间提醒用户。
在 Interface Builder 中创建视图
首先打开 JXReminderViewController.m 添加一个类扩展,声明一个 datePicker 属性,然后创建一个 addReminder:方法,向控制台输出datePicker的日期。
#import "JXReminderViewController.h" @interface JXReminderViewController () @property (nonatomic,weak) IBOutlet UIDatePicker * datePicker; @end @implementation JXReminderViewController - (IBAction)addReminder:(id)sender { NSDate * date = self.datePicker.date; NSLog(@"Setting a reminder for %@",date); } - (void)viewDidLoad { [super viewDidLoad]; } @end
接下来我们需要创建一个 XIB 文件。
创建视图对象
从对象面板库拖拽一个 UIView 对象到画布。然后拖拽一个 UIDatePicker 以及一个 UIButton .
加载 NIB 文件
当视图控制器从 NIB 文件中创建视图层次结构时,不需要覆盖 loadView 方法,默认的 loadView 方法会自动处理 NIB 文件中包含的视图层次结构。
接下来在 UIViewController 的指定初始化方法中为 JXReminderViewController 设置需要加载的 NIB 文件:
- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil
该方法包含两个参数,分别用于指定 NIB 文件的文件名及其所在的程序包。
#import "AppDelegate.h" #import "JXHypnosisViewController.h" #import "JXReminderViewController.h" @interface AppDelegate () @end @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; // 在这里添加代码初始化 JXHypnosisViewController * hvc = [[JXHypnosisViewController alloc] init]; // 获取指向 NSBundle 对象的指针,该 NSBundle 对象代表应用的主程序包 NSBundle * appBundle = [NSBundle mainBundle]; // 告诉初始化方法,在appBundle中查找 JXReminderViewController.xib 文件 JXReminderViewController * rvc = [[JXReminderViewController alloc] initWithNibName:@"JXReminderViewController" bundle:appBundle]; self.window.rootViewController = rvc; self.window.backgroundColor = [UIColor whiteColor]; [self.window makeKeyAndVisible]; return YES; }
向 NSBundle 发送 mainBundle 消息可以得到应用的主程序包。主程序包对应于文件系统中项目的根目录,包含代码文件和资源文件,初始化需要的 JXReminderViewController.xib 文件也包含在主程序包中。
到目前为止,视图层次结构中的所有视图对象都已经创建和设置好了,视图控制器的初始化方法可以正确加载 NIB 文件了。但是目前来说运行程序就会崩溃,,错误信息为
Terminating app due to uncaught exception ‘NSInternalInconsistencyException‘, reason: ‘-[UIViewController _loadViewFromNibNamed:bundle:] loaded the "JXReminderViewController" nib but the view outlet was not set.‘
这是因为 NIB 被加载时,会创建文件中的视图对象,但是这些视图对象在运行时并没有与 JXReminderViewController 关联起来,包括 JXReminderViewController 的 view 属性。当该视图控制器需要将view添加到应用窗口时,view 是 nil ,因此应用程序会崩溃。
关联 File‘s Owner
File‘s Owner 对象是一个展位符对象,他是 XIB 文件特意留下的一个 “空洞”。当某个视图控制器将 XIB 文件加载为 NIB 文件时,首先会创建 XIB 文件中的所有的视图对象,然后会将自己填入响应的 File‘s Owner 空洞,并建立之前在 Interface Builder 中设置关联。
因此,如果要在运行时关联加载 NIB 文件的对象,可在 XIB 文件中关联 File‘s Owner 。首先需设置 JXReminderViewController.xib 文件中的 File‘s Owner 是 JXReminderViewController
接下来在 XIB 文件中添加所需要的关联,,首先是 控制器的view属性,在大纲视图中按住 control 并单击 File‘s Owner ,Xcode 会显示关联面板,列出所有可用的关联。将 view 插座变量与画布中的 UIView 对象关联起来。这样当 JXReminderViewController 对象载入 XIB 文件是,其 view 属性就能自动指向画布中的 UIView 对象。
现在, JXReminderViewController 对象在运行的时候可以加载 view 了。同样的方法关联其余插座变量,首先将插座变量 datePicker 关联至 UIDatePicker 对象,然后将 UIButton 对象关联至 File‘s Owner 并选择弹出式菜单中的 addReminder:
之前的代码中将 JXReminderViewController 的 datePicker 插座变量声明为弱引用。将插座变量申明为弱引用是一种编程约定。当系统的可用内存偏少时,视图控制器会自动释放其视图并在之后需要显示时再创建。
- UITabBarController
当应用为了响应用户的操作,需要将当前显示的视图切换为另一个时,使用视图控制器的好处就更加明显。本节学习一个 UITabBarController 对象,使应用能够在 JXReminderViewController 对象和 JXHypnosisViewController 对象之间自由的切换。
UITabBarController 对象可以保存一组视图控制器,此外 UITabBarController 对象还会在屏幕底部显示一个标签栏,标签栏会有多个标签选项,分别对应 UITabBarController 对象保存的每一个视图控制器。单击某个标签项, UITabBarController 对象就会显示该标签选项所对应的视图控制器的视图。
#import "AppDelegate.h" #import "JXHypnosisViewController.h" #import "JXReminderViewController.h" @interface AppDelegate () @end @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; // 在这里添加代码初始化 JXHypnosisViewController * hvc = [[JXHypnosisViewController alloc] init]; // 获取指向 NSBundle 对象的指针,该 NSBundle 对象代表应用的主程序包 NSBundle * appBundle = [NSBundle mainBundle]; // 告诉初始化方法,在appBundle中查找 JXReminderViewController.xib 文件 JXReminderViewController * rvc = [[JXReminderViewController alloc] initWithNibName:@"JXReminderViewController" bundle:appBundle]; UITabBarController * tabBarController = [[UITabBarController alloc] init]; tabBarController.viewControllers = @[hvc,rvc]; self.window.rootViewController = rvc; self.window.rootViewController = tabBarController; self.window.backgroundColor = [UIColor whiteColor]; [self.window makeKeyAndVisible]; return YES; }
构建并运行,就能在底部自由切换两个控制器。
UITabBarController 也是 UIViewController 的子类,也有一个名为 view 的属性。UITabBarController 对象的 view 指向一个包含该两个子视图的 UIView 对象,分别是标签栏和当前选中的视图控制器的视图。
设置标签项
标签栏上的每一个标签项都可以显示标题和图片,具体数据需要由视图控制器的 tabBarItem 属性提供。当 UITabBarController 对象加入一个视图控制器时,就会为标签栏增加一个标签项,并根据新加入的视图控制器的 tabBarItem 属性设置该标签项的标题和图片。
首先,我们打开 Images.xcassets 这是用来管理项目中所有图片资源的文件,将所需的图片导入。接下来打开 JXHypnosisViewController 覆盖 UIViewController 的指定初始化方法 initWithNibName:bundle: .
#import "JXHypnosisViewController.h" #import "JXHypnosisView.h" @interface JXHypnosisViewController () @end @implementation JXHypnosisViewController - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (self) { // 设置标签项的标题 self.tabBarItem.title = @"Hypnotize"; // 从图片文件创建一个 UIImage 对象 UIImage * i = [UIImage imageNamed:@"Hypno"]; // 将 UIImage 对象赋值给标签项的 iamge 属性 self.tabBarItem.image = i; } return self; } - (void)viewDidLoad { [super viewDidLoad]; } - (void)loadView { // 创建一个 JXHypnosisView 对象 JXHypnosisView * backgroundView = [[JXHypnosisView alloc] init]; // 将 JXHypnosisView 对象赋给视图控制器的view 属性 self.view = backgroundView; } @end
在 JXReminderViewController 同样的操作
#import "JXReminderViewController.h" @interface JXReminderViewController () @property (nonatomic,weak) IBOutlet UIDatePicker * datePicker; @end @implementation JXReminderViewController - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (self) { // 设置标签项的标题 self.tabBarItem.title = @"Reminder"; // 从图片文件创建一个 UIImage 对象 UIImage * i = [UIImage imageNamed:@"Time"]; // 将 UIImage 对象赋值给标签项的 iamge 属性 self.tabBarItem.image = i; } return self; } - (IBAction)addReminder:(id)sender { NSDate * date = self.datePicker.date; NSLog(@"Setting a reminder for %@",date); } - (void)viewDidLoad { [super viewDidLoad]; } @end
- 视图控制器的初始化方法
之前在创建 JXHypnosisViewController 的标签项时覆盖了 initWithNibName:bundle: 但是,在 AppDelegate 中是使用 init 方法来初始化 JXHypnosisViewController 对象的。这是由于 initWithNibName:bundle: 是 UIViewController 的指定初始化方法,向视图控制器发送 init 会调用 initWithNibName:bundle: 方法,并为两个参数都传入 nil ,因此使用 init 初始化 也可以正确设置参数。
- 添加本地通知
接下来使用本地通知。本地通知用于向用户提醒一条消息,除了闹钟,其他的并没有什么卵用。
实现本地通知很简单,首先需要创建一个 UILocalNotification 对象并设置其显示内容和提醒时间,然后调用 UIApplication 单粒对象的 scheduleLocalNotification: 方法注册该通知就可以了。
#import "JXReminderViewController.h" @interface JXReminderViewController () @property (nonatomic,weak) IBOutlet UIDatePicker * datePicker; @end @implementation JXReminderViewController - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (self) { // 设置标签项的标题 self.tabBarItem.title = @"Reminder"; // 从图片文件创建一个 UIImage 对象 UIImage * i = [UIImage imageNamed:@"Time"]; // 将 UIImage 对象赋值给标签项的 iamge 属性 self.tabBarItem.image = i; } return self; } - (IBAction)addReminder:(id)sender { NSDate * date = self.datePicker.date; NSLog(@"Setting a reminder for %@",date); UILocalNotification * note = [[UILocalNotification alloc] init]; note.alertBody = @"Hypnotize me!"; note.fireDate = date; [[UIApplication sharedApplication] scheduleLocalNotification:note]; } - (void)viewDidLoad { [super viewDidLoad]; } @end
构建并运行,选中提醒时间即可。
- 加载和显示视图
现在 JXHypnoNerd 应用中有两个视图控制器。当应用启动后,标签栏会默认显示第一个视图控制器的视图,这时第二个视图控制器的视图不需要显示,只有当用户点击了第二个才会显示相应的视图。为了实现视图延迟加载,在 initWithNibName:bundle: 中不应该访问 view 或 view 的任何子视图。凡是和 view 或 view 的子视图相关的初始化代码,都应该在 viewDidLoad 方法中实现,避免加载不需要再屏幕上显示的视图。
访问视图
通常情况下,在用户看到 XIB 文件中创建的视图之前,需要对他们做一些额外的初始化工作,但是,关于视图的初始化代码不能写在视图控制器的初始化方法中(此时视图控制器并没有加载 NIB 文件,所有指向视图的属性都是nil)。如果向这些属性发送消息,虽然编译的时候不会报错,但是运行的时候无法对这些属性做任何操作。
那么,我们在应该在哪些方法中访问 XIB 文件中的视图呢?主要包括两个方,可以根据实际需要选择。第一个方法是用于确认视图已经加载的 viewDidLoad ,该方法会在视图控制器加载完 NIB 文件之后调用,此时视图控制器中所有视图属性都已经指向了正确的视图对象。第二个方法是 viewWillAppear: 该方法会在视图控制器的 view 添加到应用窗口之前被调用。
两个方法的区别是,如果只需要在应用启动后设置一次视图对象,就选择 viewDidLoad 如果用户每次看到视图控制器的 view 都需要对其进行设置就应该选择 viewWillAppear
- 与视图控制器及其视图进行交互
application: didFinishLaunchingWithOptions: 在该方法中设置和初始化应用窗口的根视图控制器。该方法只会在应用启动完毕后调用一次,之后如果从其他应用切换回本应用,则该方法不会再次被调用。
initWithNibName:bundle: 该方法是 UIViewController 的指定初始化方法,创建视图控制器时,就会调用该方法。
loadView 可以覆盖该方法,使用代码方式设置视图控制器的view属性。
viewDidLoad 可以覆盖该方法,设置使用 NIB 文件创建的视图对象。该方法会在视图控制器加载完视图后被调用。
viewWillAppear: 可以覆盖该方法,设置使用 NIB 文件创建的视图对象。该方法和 viewDidAppear: 会在每次视图控制器的 view 显示在屏幕上时被调用。