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的動作

2012年12月30日 星期日

NSString 的 property attribute

假設有某個class,他有一個NSString*的property時,那該怎麼設計這個property的attributes呢 ?

@interface MyClass : NSObject

@property /* ( ??? ) */ NSString* Name;

@end

首先,by default,所有的property都是atomic,也就是說會自動implement thread safety,所以如果只是UI端會用到的property的話,通常是要設定成nonatomic。

接著的問題是,在ARC底下,property的default都是strong,也就是說MyClass的Name會跟他的caller所pass進來的NSString*指向同一個地方。

由於NSString*是immutable,所以似乎strong看起來是個不錯的設定值。可是,由於caller可能會傳入一個NSMutableString (derived from NSString),如果是在這樣子的情形底下的話,就無法保證MyClass的Name的值不會被改到了 !

所以大部分的情形底下,應該都是要設定成copy !


@interface MyClass : NSObject

@property (nonatomic, copy) NSString* Name;

@end


2012年12月29日 星期六

XCode Shortcut Tips

XCode一直用的不是很順手,尤其時要切換檔案時好像除了透過Project Navigator之外別無其他方法,這樣子就一定得用到滑鼠才行。

今天找到了幾個hotkey,操作起來比較順手一些,所以把他記錄下來。

  1. Command+0:這個可以show/hide 左邊Project Navigator
  2. Option+Command+0:這個可以show/hide右邊的Utility window
  3. Ctrl+5:Show Group Files,這個指令會顯示一個popup window,裡面列出project內的所有檔案。我把這個command改成Ctrl+Tab,這樣子的操作習慣就比較像Visual Studio的切換檔案的方式
Happy coding...


2012年12月24日 星期一

How to debug iOS program: the first step

當程式crash時,通常看到的畫面都是像這個樣子的:

程式break在main loop,從console的地方可以看到某個exception,可是不知道exception是從哪裡發出來的!

如果希望在程式一遇到exception時就馬上停下來(誰不希望這個樣子呢?),那我們可以這樣子做:

第一步先從Project Navigator內切換到Debug(?) tab,然後新增加一個breakpoint,如下


然後選擇Add Exception Breakpoints,畫面如下:



按Done之後,重新執行程式,xcode應該就會在程式要送出exception時把程式停在該停的地方了 !

For AppCode,可以從Run menu內選擇View Breakpoints,然後enable Exception breakpoints:



這樣子就大功告成了!!


Image Buttons, and when there are too many

延續計算機的習題, 今天的任務是要把其他的按鈕加到程式內。

要建立像這樣子的按鈕,我們可以使用UIButton,指定他的buttonface image,以及highlight image。由於button不是正方形,而且我們希望以後調整button大小時不需要重新製作image files,所以同樣的我們必須使用caps的方式來讓iOS幫我們自動調整影像大小。

建立一個image button的sample code大概是像這個樣子

[see sample code here]

注意到我們必須使用resizableImageWithCapInsets的方式來load image。另外PNG image檔案內必須定義外框的alpha channel,以便可以做出非正方形的效果。

OK,現在我們知道怎麼做出有影像的buttons了,可是接下來的問題是,畫面上這麼多個buttons,難道真的要用Interface Builder一個一個ctrl-drag到ViewController內嗎?這樣子程式不是變得很亂嗎?

一個很簡單的方法是,從Interface Builder內指定每個button的unique tag。Tag是一個number,所以我們可以把它當成Win32的control ID來使用,然後從程式內透過以下的方式來取得這個control:


// Return the button with the specified ID
//
- (UIButton*) getButton:(int)idButton
{
    return [self.view viewWithTag:idButton];
}

這樣子一來程式就變得比較乾淨了!

另外在設計button的內容時遇到了一個問題:怎麼設定 +,-, *, /, 這幾個button?如果是用一般的ASCII字元的話,看起來很難看。難道要自己畫不成?

類似的問題在StackOverflow上也可以找到解答:從Mac的menu bar上,click Language icon,在popup menu內會看到Show Character Viewer的選項,從這裡面就可以找到很多特殊字元,直接double click,就可以把對應的unicode paste到IB內了。

完成品如下: