接下來在故事述說的過程裡,彼得潘將指出故事情節所對應的專案程式碼。若是沒有特別提到對應的專案程式碼,即表示此處的動作是存在,真的有執行,但我們看不到也不需要去煩心尋找修改的。
故事一(以xib設計App起始畫面):
1. App啟動,執行main function
檔案 main.m
int main(int argc, char *argv[])
{
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
2. 建立代表App的UIApplication物件
3. 建立App代理人AppDelegate物件,UIApplication物件的代理人將被設為此AppDelegate物件
為什麼那麼巧App的代理人物件類別是AppDelegate? 其實這是有玄機的。回頭看看剛剛的main function,在main function裡我們呼叫UIApplicationMain function,此function的宣告如下:
檔案 UIApplication.h
int UIApplicationMain(int argc, char *argv[], NSString *principalClassName, NSString *delegateClassName);
此function的第4個參數為delegateClassName。所以我們在此傳入代理人物件的類別字串,到時候自然就會產生此類別的代理人物件。 最簡單的寫法其實可以直接傳入@"AppDelegate",不過在這裡我們看到了實際傳入的則是以下function的回傳物件
NSStringFromClass([AppDelegate class])
此function的宣告如下:
檔案NSObjCRuntime.h
NSString *NSStringFromClass(Class aClass);
NSStringFromClass([AppDelegate class])
此function的宣告如下:
檔案NSObjCRuntime.h
NSString *NSStringFromClass(Class aClass);
在SDK裡,每一個類別都可以化身為類別物件( 類別為Class),而當我們將類別物件傳入NSStringFromClass後,即可取得代表此類別的NSString物件。因此我們首先呼叫[AppDelegate class]取得AppDelegate的類別物件 (任何的類別都可透過呼叫class method,得到它的類別物件) ,接著當成參數傳入後,即可取得此類別的字串@"AppDelegate"。
class method的宣告如下:
檔案NSObject.h
+ (Class)class;
4. App完成啟動,呼叫AppDelegate物件的application:didFinishLaunchingWithOptions: method
檔案AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
self.viewController = [[ViewController alloc] initWithNibName:@"ViewController" bundle:nil];
self.window.rootViewController = self.viewController;
[self.window makeKeyAndVisible];
return YES;
}
由application:didFinishLaunchingWithOptions: 的程式碼,可以再衍生出步驟5 ~ 8
5. 建立App主畫面的window (UIWindow物件),並設定AppDelegate物件的window property連結到window物件
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
UIWindow物件是App畫面最底層的UI元件,必須要先有window,之後才能再將其它的UI元件,比方UIView元件,加到它的上面。
在建立UIWindow元件時,我們透過nitWithFrame: method初始化window的大小。由於App的畫面一定是佔滿整個螢幕,所以我們利用[[UIScreen mainScreen] bounds]取得螢幕的大小,以此作為window的尺寸。
至於AppDelegate的window property,則可在AppDelegate.h裡找到
檔案 AppDelegate.h
@property (strong, nonatomic) UIWindow *window;
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
UIWindow物件是App畫面最底層的UI元件,必須要先有window,之後才能再將其它的UI元件,比方UIView元件,加到它的上面。
在建立UIWindow元件時,我們透過nitWithFrame: method初始化window的大小。由於App的畫面一定是佔滿整個螢幕,所以我們利用[[UIScreen mainScreen] bounds]取得螢幕的大小,以此作為window的尺寸。
至於AppDelegate的window property,則可在AppDelegate.h裡找到
檔案 AppDelegate.h
@property (strong, nonatomic) UIWindow *window;
6. 建立View Controller物件,指定其對應的xib檔名,並連結到AppDelegate的property
self.viewController = [[ViewController alloc] initWithNibName:@"ViewController" bundle:nil];
AppDelegate的viewController property宣告如下:
檔案 AppDelegate.h
@property (strong, nonatomic) ViewController *viewController;
利用initWithNibName:bundle:初始化ViewController物件。此method的宣告如下:
檔案 ViewController.h
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil;;
AppDelegate的viewController property宣告如下:
檔案 AppDelegate.h
@property (strong, nonatomic) ViewController *viewController;
利用initWithNibName:bundle:初始化ViewController物件。此method的宣告如下:
檔案 ViewController.h
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil;;
參數nibNameOrNil:
xib檔的名稱。
參數nibBundleOrNil:
NSBundle物件。到時候在載入xib檔時,將會尋找bundle下的xib檔。由於我們的xib檔是直接加入專案裡,所以它會包含在最後產生的App bundle資料夾下。因此其實我們應該傳入[NSBundle mainBundle],如此搭配前一個參數傳入的xib檔名@"ViewController",即可找到ViewController.xib。
但是令人不解的,我們卻傳入nil。這是因為當我們傳入nil時,到時候在載入xib時,將假設此xib在App bundle資料夾下,因此還是可以找到xib檔。
如果仔細觀察UIViewController.h,則可找到以下2個property。
@property(nonatomic, readonly, copy) NSString *nibName;
@property(nonatomic, readonly, retain) NSBundle *nibBundle;
因此我們在initWithNibName:bundle:傳入的參數,其實會記錄在這2個property裡。等到之後時機成熟,真的要載入xib時,再從這裡查詢。
xib檔的名稱。
參數nibBundleOrNil:
NSBundle物件。到時候在載入xib檔時,將會尋找bundle下的xib檔。由於我們的xib檔是直接加入專案裡,所以它會包含在最後產生的App bundle資料夾下。因此其實我們應該傳入[NSBundle mainBundle],如此搭配前一個參數傳入的xib檔名@"ViewController",即可找到ViewController.xib。
但是令人不解的,我們卻傳入nil。這是因為當我們傳入nil時,到時候在載入xib時,將假設此xib在App bundle資料夾下,因此還是可以找到xib檔。
如果仔細觀察UIViewController.h,則可找到以下2個property。
@property(nonatomic, readonly, copy) NSString *nibName;
@property(nonatomic, readonly, retain) NSBundle *nibBundle;
因此我們在initWithNibName:bundle:傳入的參數,其實會記錄在這2個property裡。等到之後時機成熟,真的要載入xib時,再從這裡查詢。
7. 設定UIWindow物件的rootViewController property為剛剛建立的ViewController物件
self.window.rootViewController = self.viewController;
此property的宣告如下
此property的宣告如下
檔案 UIWindow.h
@property(nonatomic,retain) UIViewController *rootViewController;
8. 呼叫UIWindow物件的makeKeyAndVisible,顯示window
[self.window makeKeyAndVisible];
最後呼叫的makeKeyAndVisible method,其實是App畫面現身的關鍵一步。當呼叫此method時,除了代表讓window顯示外,還會去呼叫此window的rootViewController的view method,取得controller管理的畫面_view。在這裡view method其實就是_view的getter,它們的相關宣告如下:
檔案UIViewController.h
UIView *_view;
@property(nonatomic,retain) UIView *view;
此getter method view十分特別。傳統上我們會經由setter設定,getter取得。但此getter會去檢查此時_view是否有值,如果沒有了話它會去生成_view元件,然後才回傳。因此在這一步時它會先發現此時_view是空的,於是經由nibName和nibBundle找尋相關的xib檔,如果有找到,即會呼叫NSBundle物件的loadNibNamed:owner:options: method,載入此xib檔,生成xib檔裡的UI元件。此method的宣告如下:
檔案UINibLoading.h
- (NSArray *)loadNibNamed:(NSString *)name owner:(id)owner options:(NSDictionary *)options;
在呼叫此method時,此controller物件將是第2個傳入的參數owner,因此到時候此xib檔的owner將是此controller。
接著切換到Connection Inspector頁面,我們可以看到controller的view已經事先被連結到畫面上的View元件了。所以回到剛剛前面載入xib檔的說明。當xib檔被載入,裡面的UI元件一一被生成後,它的UIView元件就連結到了controller的view。因此最後這個view,也就是我們在xib裡設計的畫面,將被加到window上,成為我們啟動App後,第一個看到的畫面。
故事二(以程式碼設計App起始畫面):
當不靠storyboard,也不透過xib,完全經由程式碼打造App美麗的起始畫面時,故事又將如何進行呢? 其實答案很簡單,毫無創意,幾乎和故事二一模一樣。如下所示,我們看到它的application:didFinishLaunchingWithOptions: ,跟故事一的就像是同一個模子印出來的
檔案AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Override point for customization after application launch.
self.viewController = [[ViewController alloc] init];
self.window.rootViewController = self.viewController;
[self.window makeKeyAndVisible];
return YES;
}
唯一的小小差異,存在於以下這一行controller物件的建立。
self.viewController = [[ViewController alloc] init];
在這裡我們單純的建立ViewController物件,不依靠xib,因此沒有呼叫 initWithNibName:bundle:。到時候controller顯示的畫面完全定義在ViewController.m裡。
其實有一個秘密,彼得潘忘了提。雖然我們只呼叫init,但是在UIViewController定義的init method裡,其實會另外地再去呼叫initWithNibName:bundle: method,只不過此時傳入的2個參數都是nil。前面提到當nibBundleOrNil為nil時,將預設為App的main bundle。其實當nibNameOrNil等於nil時,也有特別意義。當它是nil時,將假設要找尋的xib檔檔名跟controller同名。所以如果我們的xib檔跟controller同名,比方在這裡都叫ViewController,其實是可以傳入nil就好。
因此,最後呼叫makeKeyAndVisible時,一樣會試著去載入ViewController.xib,只不過因為此時專案裡沒有這個檔案,所以到時候將另外生成什麼都沒有的view元件做為controller的view。
其實有一個秘密,彼得潘忘了提。雖然我們只呼叫init,但是在UIViewController定義的init method裡,其實會另外地再去呼叫initWithNibName:bundle: method,只不過此時傳入的2個參數都是nil。前面提到當nibBundleOrNil為nil時,將預設為App的main bundle。其實當nibNameOrNil等於nil時,也有特別意義。當它是nil時,將假設要找尋的xib檔檔名跟controller同名。所以如果我們的xib檔跟controller同名,比方在這裡都叫ViewController,其實是可以傳入nil就好。
因此,最後呼叫makeKeyAndVisible時,一樣會試著去載入ViewController.xib,只不過因為此時專案裡沒有這個檔案,所以到時候將另外生成什麼都沒有的view元件做為controller的view。
故事三(以storyboard設計App起始畫面) :
前三個步驟跟故事一相同,但從步驟4就開始變調了。
4. 建立App主畫面的window (UIWindow物件),並設定AppDelegate物件的window property連結到window物件。
5. 載入MainStoryboard.storyboard
從Target App Summary頁面的Main Storyboard欄位我們可以設定App啟動後載入的storyboard檔
也可以從Info頁面的Main storyboard file base name欄位設定。
6. 從storyboard找到負責起始畫面的view controller (由箭頭標記識別)
在Storyboard上可能有多個view controller,負責起始畫面的view controller有以下2個特徵
(1) 有個箭頭指向它
(2) Attribute Inspector頁面的Is Initial View Controller被勾選
7. 建立負責起始畫面的view controller物件
在view controller的Identity Inspector頁面裡,Class欄位決定了view controller的類別。
因為View Controller的view property連結到storyboard上設計的view,所以到時候我們看到的App畫面,也就是此controller所管理的頁面,才會恰恰好等於storyboard上設計的畫面。
8. 設定UIWindow物件的rootViewController property為剛剛建立的ViewController物件
9. App完成啟動,呼叫AppDelegate物件的application:didFinishLaunchingWithOptions: method
檔案AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Override point for customization after application launch.
return YES;
}
令人覺得不可思議的,當以storyboard設計App起始畫面時,application:didFinishLaunchingWithOptions:裡竟然什麼都不做,只回傳YES表示App順利完成啟動。其實當初故事一二的application:didFinishLaunchingWithOptions:裡做的事,在故事三也有做,只是這些事都是早在此method被呼叫前就事先做了。所以如果我們在此method裡列印self.window, self.viewController時,將看到物件的記憶體位址,而不會是空的。
10. 呼叫UIWindow物件的makeKeyAndVisible method,顯示window,進行和前面故事一二一樣的流程。只不過這次生成的view將從storyboard而來,連結到storyboard裡的UIView元件。
感謝你詳細的講解,有一點不清楚:
回覆刪除故事一中的第9點,project內有用到UIWindow.h嗎?還是有class 是繼承UIWindow,所以具有rootViewController property?因為project內看不到在哪有去執行 “設定UIWindow物件的rootViewController property為剛剛建立的ViewController物件”
同樣,第11點也沒有地方看到有“呼叫UIWindow物件的makeKeyAndVisible method”
最後,是不是ViewController 這個class 從來都沒有被instantiate成object的需要?
不好意思,基礎太差,問題一堆!
Neo上
你好,我已經重新修改文章,加了以下描述,
回覆刪除"接下來在故事述說的過程裡,彼得潘將指出故事情節所對應的專案程式碼。若是沒有特別提到對應的專案程式碼,即表示此處的動作是存在,真的有執行,但我們看不到也不需要去煩心尋找修改的。“
至於ViewController物件的建立,在故事一時,它是發生在step 8,只是我們看不到它的程式碼。而在故事二和故事三,則都是發生在step 6,並且可以清楚地看到產生它的程式碼。
謝謝你的回覆,有所領悟,消化當中⋯⋯
回覆刪除Nice Sharing ! 非常詳盡的介紹. 讚!
回覆刪除