2013年2月16日 星期六

viewDidLoad的處理邏輯

最近看了一篇文章,裡面提到了viewDidLoad的相關流程,發現這裡面大有學問,所以把文章內提到的相關問題記錄在底下。

一般而言,我們都會在viewDidLoad內執行view的UI的initialization。如果view所需要的資料是從外部pass進來的話,那viewDidLoad內就應該是把這個資料的內容顯示到對應的controls上面。

可是,由於viewDidLoad可能會被執行多次,所以某些邏輯可能要很小心處理。

那到底在什麼樣子的情形下,viewDidLoad會被執行多次呢?答案是,如果device是iOS 5.1以前的版本的話,那螢幕上看不見的view可能會在memory low的時候被unload,所以當需要重新顯示這個view時,這個view的viewDidLoad就會再次被執行!

以下面的畫面流程舉例:


畫面一開始是顯示Add CheckList(ControllerA),所以此時ControllerA被created,同時執行viewDidLoad。當使用者按了Icon後面的箭頭符號時則顯示Select Icon畫面(ControllerB)。假設此時device的memory不足的話,系統可能會直接unload view A。然後當使用者按了Back button時,再重新load view A (再次呼叫viewDidLoad)。注意到在這種情形底下,ControllerA只被create一次,可是viewDidLoad卻被呼叫兩次。

一般而言,ControllerA可能會在viewDidLoad時根據傳入的資料去設定editbox以及icon的初始值。那我們先假設傳入的text是空的,icon也是空的,所以畫面上editbox被initialized成空白,同時icon被設定成None。

接著使用者在editbox內輸入"ABC",按了icon後面的箭頭,進入了第二個畫面,然後因為memory不足的緣故導致第一個畫面被unload。

然後使用者按了Back button。

如果是照上面的implementation的方式的話,viewDidLoad會檢查傳入的資料,所以又會再次把editbox清成空白,而不是把它填成最後一次使用者所輸入的"ABC"!

如果要正確的處理viewDidLoad被呼叫多次的情形的話,程式必須把畫面上所有可能被修改到的control的狀態儲存下來(as ivars),然後在viewDidLoad時使用這些ivar去initialize畫面上的controls,而不是利用這個Controller的參數介面來initialize畫面上的controls。

我們假設ControllerA有一個CheckList object property是由外部傳入用來決定畫面的初始值,程式的部份我們必須如下處理:

 // Called when view is initialized (called exactly once)  
 //    - initialize internal data  
 //  
 - (id)initWithCoder:(NSCoder*)aDecoder  
 {  
     self = [super initWithCoder:aDecoder];  
     if (self)  
     {  
         [self doInitData];  
     }  
     return self;  
 }  

其中doInitData的動作是把這個CheckList object的內容轉換成內部的ivars

 @interface EditCheckListViewController()  
 // Keep track of current name & iconIndex  
 //  
 @property (nonatomic, copy)NSString* name;  
 @property (nonatomic, assign)int iconIndex;  
 @end  
 
 // Initialize internal data  
 //  
 - (void)doInitData  
 {  
     if (self.checkList != nil)  
     {  
         self.name = self.checkList.name;  
         self.iconIndex = self.checkList.iconIndex;  
     }  
     else  
     {  
         self.name = @"";  
         self.iconIndex = ICON_NONE;  
     }  
 }  

接著在viewDidLoad的時候我們改成用self.name以及self.iconIndex來initialize UI:

 - (void)viewDidLoad  
 {  
     [super viewDidLoad];  
     [self doInitView];  
 }  
 // Initialize view based on self.checkList object  
 //  
 - (void)doInitView  
 {  
     if (self.checkList != nil)  
         self.title = @"Edit CheckList";  
     else  
         self.title = @"Add CheckList";  
     self.textName.text = self.name;  
     [self configureIcon];  
 }  

除此之外,當textName或者是icon相關的control有做了任何修改時,程式必須馬上update self.name以及self.iconIndex,以確保下次viewDidLoad被呼叫時可以正確的設定畫面的內容。

除了viewDidLoad的流程之外,程式也必須在遇到記憶體不足時把所有的IBOutlet nil掉,以便iOS可以把所有的memory拿回去,一般書籍文章則多是建議程式在viewDidUnload時做這個動作。

可是可能是這個部分的原始設計太過複雜導致很多application的作法不一致,或者是Apple覺得新的device的記憶體已經很大了,也許可以簡化系統的流程,所以在iOS 6以後,系統已經修改成在記憶體不足時不再unload view,同時也把viewDidUnload變成是deprecated的API。

可是如果程式必須support iOS 6之前的版本的話,那該怎麼寫比較好呢?

以下是作者提出來的作法,可以用同樣的邏輯處理不同的iOS版本。

 - (void)didReceiveMemoryWarning  
 {  
     [super didReceiveMemoryWarning];  
     if (self.isViewLoaded && self.view.window == nil)  
         self.view = nil;  
     self.btnDone = nil;  
     self.textName = nil;  
     self.labelIconName = nil;  
     self.imageIcon = nil;  
 }  

我們把原先寫在viewDidUnload的code搬到didReceiveMemoryWarning。當收到這個notification時,如果是iOS 6之前的版本的話,view已經被unload了,所以我們只需把相關的IBOutlet nil掉就行了。可是如果是iOS 6的話,則程式會呼叫self.view = nil來把view unload掉。所以當下次需要重新顯示這個view時,iOS就會執行load view的動作,也就會呼叫到viewDidLoad。這樣子的話不同版本的邏輯就會一致了!

一個很常見的流程(viewDidLoad),卻因為之前舊版OS的設計方式,導致必須用非常複雜的邏輯才能做對,這個應該算是個design flaw吧。





2013年2月10日 星期日

Connection outlet/action in XCode

如果要從XCode的Interface builder去connect outlet or action的話,一般的tutorial都會啟動assistent editor,然後從storyboard的UI上 control-drag component to assistent editor內對應的程式碼。這個作法雖然很好,可是由於assistent editor很佔空間,所以如果storyboard比較複雜的話,操作起來總是手忙腳亂。

另外一個作法,是先手動在controller的header file內宣告IBOutlet and/or IBAction,然後透過Utility window內的Connection Inspector來建立關連性。

操作方式請參考以下的圖示:



UITextField的相關設定

以下的圖示是一個簡單的資料輸入畫面



在這個畫面內有幾個技巧要注意:

  1. 在viewWillAppear時必須把text field設定成firstResponder,這樣子當view開啟時就會自動顯示keyboard,
  2. 從interface builder內設定這個text field的Return Key為Done,這個動作會讓keyboard上面的Return自動變成"Done",
  3. 從interface builder內enable這個text field的Auto enable Return key,這樣子的話如果沒有輸入任何文字的話,keyboard上的Done button就會自動disable,
  4. 從interface builder內,選擇這個text field,然後從Connection Inspector內把[Did End On Exit] event連結到處理[done]的handler,這樣子當user按了keyboard上面的enter時,就等同於trigger done的動作