視圖控制器是 UIViewController 類或其子類對象。每個視圖控制器都負責管理一個視圖層次結構,包括創建視圖層級結構中的視圖並處理相關用戶事件,以及將整個視圖層次結構添加到應用視窗。 創建一個程式,並將上節 JXHypnosisView 類導入到工程中。 創建 UIViewControll ...
視圖控制器是 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 顯示在屏幕上時被調用。