顯示具有 iOS 標籤的文章。 顯示所有文章
顯示具有 iOS 標籤的文章。 顯示所有文章

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:



這樣子就大功告成了!!


2012年12月22日 星期六

iOS內如何寫出一個有Background Image的Label, Part 2

繼續前一篇文章內提到的有背景圖片的UILabel的實作方式,這一次試著直接subclass UILabel,然後加入padding以及background image的屬性設定.


// ImageLabel.h
//


@interface ImageLabel : UILabel

// Text Padding
//
@property (nonatomic, assign) UIEdgeInsets insets;

- (void) setBackgroundImage:(UIImage*) image;

@end

// ImageLabel.m
//
#import "ImageLabel.h"

@implementation ImageLabel

@synthesize insets = _insets;

- (void)drawTextInRect:(CGRect)rect 
{
   return [super drawTextInRect:UIEdgeInsetsInsetRect(rect, self.insets)];
}

- (void)setBackgroundImage:(UIImage *)image
{
  CGRect rect = self.bounds;
  UIImage* imageScaled = [self imageWithImage:image scaledToSize:rect.size];

  self.backgroundColor = [UIColor colorWithPatternImage:imageScaled];
  imageScaled = nil;
}

- (UIImage*)imageWithImage:(UIImage*)image
               scaledToSize:(CGSize)newSize;
{
  UIGraphicsBeginImageContext( newSize );
  [image drawInRect:CGRectMake(0,0,newSize.width,newSize.height)];
  UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext();
  UIGraphicsEndImageContext();

  return newImage;
}

@end

另外在controller內,放入一個UILabel物件,然後把它的base class改成ImageLabel,接著在viewDidLoad內,設定UILabel的屬性:

// Initialize result label
//
- (void)initResultLabel
{
  // Stretch the image with 1x1 caps on each side
  //
  UIImage* imageBackground = [[UIImage imageNamed:@"Rez/iPhone/bg-display.png"] stretchableImageWithLeftCapWidth:1 topCapHeight:1];
  
  [_resultLabel setBackgroundImage:imageBackground];
    imageBackground = nil;

  _resultLabel.insets = UIEdgeInsetsMake(2, 4, 2, 4);
}

這樣子的效果就跟原先的設計一樣,而且更簡潔。


上述程式碼雖然可行,不過我還不確定ImageLabel是否還必須處理以下的狀況?
  1. 有沒有可能setBackgroundImage時ImageLabel的frame size還沒有被設好?
  2. ImageLabel如果被resize的話,那該怎麼處理?
這個就等待下次有空時再來研究了。




2012年12月21日 星期五

iOS內如何寫一個有background image的label

給自己一個任務: 看能否寫出一個簡單的計算機程式. 在App store上面找幾個範例, 決定參考底下這個程式的layout:

所以我想,就先來implement上面的結果區吧!

用PhoneView把程式的bundle打開,看到一個background image file,長得像底下這樣子:


影像的長寬是 615 x 205,與上方顯示區的大小不一樣 !

所以我想,應該是想辦法放一個UILabel,然後設定他的background image是這張圖,iOS應該會自動調整影像的大小,可是我還要注意不要讓iOS把影像的邊緣給弄糊掉了,iOS內應該有類似Android Nine-Patch的觀念才對。

所以去網路上找了一些code,寫成了這個樣子:
    
    // Stretch the image with 1x1 caps on each side
    //
    UIImage* imageBackground = 
       [[UIImage imageNamed:@"Rez/iPhone/bg-display.png"]             
           stretchableImageWithLeftCapWidth:1 topCapHeight:1];
    
   _resultLabel.backgroundColor
       [UIColor colorWithPatternImage:imageBackground];
    
可是這個並不work!當圖片大小與Label不一致時,colorWithPatternImage並不會自動調整圖片大小,而是會做pattern fill,所以無法達成我要的效果。

到StackOverflow上面查了一下,有人suggest改用UITextField。由於UITextField可以指定backgroundImage,所以可以自動scale圖片。唯一要注意的只要把UITextField設定成是readonly就行了。

所以我做了第二次嘗試:

    UIImage* imageBackground = ..  

    _resultText.borderStyle = UITextBorderStyleNone;
    _resultText.background = imageBackground;

這樣子看到的效果是像這個樣子:


背景影像的scale以及邊緣的處理都是正確的,可是文字太靠右邊了,我需要一些padding!

再次查詢,這時候發現問題越來越大了,有的作法是subclass TextField,然後自定他文字的bounding box,有的作法是在TextField的右緣再放入一個空的view,我則是先試看看這樣子的作法:
  • 在TextField的下方放一個UIView,設定UIView的背景為底圖,
  • 把TextField改成是透明的,然後position TextField來產生padding的效果
這時候又發現,UIView也是沒有backgroundImage property,只有backgroundColor(跟UILabel一樣),所以圖片還是只能用pattern fill的方式擺放!看來還是必須想辦法自己寫code來scale background image。以下是相關的程式碼:

    UIImage* imageBackground = ..  

    // resize image to fit _resultTextBackground
    //
    CGRect rect = _resultTextBackground.bounds;
    UIImage* imageScaled = 
      [self imageWithImage:imageBackground scaledToSize:rect.size];

    _resultTextBackground.backgroundColor = 
      [UIColor colorWithPatternImage:imageScaled];
    
    _resultText.borderStyle = UITextBorderStyleNone;
    [_resultText setBackgroundColor:[UIColor clearColor]];
    
    const int cxPadding = 4;
    const int cyPadding = 4;
    _resultText.bounds = CGRectMake(cxPadding, cyPadding, 
       rect.size.width - cxPadding * 2, rect.size.height - cyPadding * 2);

另外

- (UIImage*)imageWithImage:(UIImage*)image
              scaledToSize:(CGSize)newSize;
{
   UIGraphicsBeginImageContext( newSize );
   [image drawInRect:CGRectMake(0,0,newSize.width,newSize.height)];
   UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext();
   UIGraphicsEndImageContext();

   return newImage;
}

這樣子的效果就對了

只是我開始想到其他的問題:
  1. imageWithImage這個是個common function,從xcode的角度該用什麼方式來建構library呢?or 這個API可以用category的方式加到UIImage class內 ?
  2. 如果我必須自己scale image的話,那也許直接用UILabel也可以做的出來?
  3. 這code看起來已經有點亂了,也許要試著把這組control寫成一個獨立的元件來用?
問題1跟3是很基礎的架構性的問題,希望下次可以找到好的方法!!


2012年12月17日 星期一

Add Resource File to XCode的各種不同方式

以下整理把檔案/Folder加入XCode專案時的各種不同選項。

不管是從Finder內直接拖曳檔案/Folder進入XCode,或是從XCode內執行File | Add Files to ... ,XCode都會顯示以下的畫面讓user決定檔案加入的方式:


如果是放入單一檔案的話,則唯一重要的選項是是否選擇Copy items into destination group's folder。如果選擇了這個項目的話,則檔案會被複製到project的目錄底下,從此以後任何的修改就以project內的這個檔案的內容為主,與原始檔案無關。如果不選擇的話,則project內看到的檔案會是一個reference to 原始檔案,使用者就必須確認這個檔案與project的目錄相對關係。

如果是放入一個目錄的話,則Folder內的選項會決定這些新加入的檔案的Folder架構。
如果選擇[Create groups for any added folders]的話,則xcode會依照原始目錄的架構把檔案加到project內,可是目錄的架構會以Group的方式來模擬,從UI上會看到一個黃色的Group icon


反之如果選擇[Create folder reference]的話,則xcode會產生一個藍色的Folder icon,如上圖。

這兩者最大的差異是,Group的架構看起來雖然像是個folder,可是實際上檔案都是攤平在同一層的,所以原先的目錄如果是以Group的方式加入之後,最後產生的app檔案內所有的檔案都是放在同一層內,程式內如果要reference這些檔案的話就不需要傳入Group的path。如果是Create folder reference的話,那原始目錄的架構還是會繼續維持,程式內要讀取這些檔案時就必須注意檔案的path。

上述兩者選項都可以搭配[Copy items into destination group]選項。如果選擇的話,則xcode會把檔案或是目錄copy到project folder內。注意到如果不是[Create folder reference]的話則原始目錄內的所有檔案會被copy到project folder內的同一層。 如果不選擇的話,則xcode會記錄原始目錄的位置,所以使用者在移動project也要注意目錄的相對關係。

如果一個專案內需要用到很多的檔案時,最好是可以有一個目錄架構以方便管理,這時候應該是選擇[Create folder reference],然後optionally的搭配[Copy items]選項來決定以後要在哪個目錄內管理這些檔案。





2012年12月16日 星期日

如何manage image resource files

當我們有大量的image files/folders需要加到xcode的專案內時, 最好的方式是讓這些folder/files以reference的方式加到專案內, 這樣子的話我們可以直接從file system上面去maintain這些檔案, 而且從xcode內看到的樣子也會自動跟file system上面的是一致的.

(請參考以下這一篇的作法: http://majicjungle.com/blog/123/)

為了確認xcode build出來的app的layout跟我們想的一樣, 可以從xcode內把build的output file放置到project folder底下, 作法如下 (XCode 4.5.2)

XCode | Preference | Locations, select Derived Data, Advanced, 然後把Build location設定成Relative to Workspace, 如下圖:
這樣子的話每次build之後在project的目錄底下就會有一個Build的目錄, 可以從裡面找到build出來的application bundle, 透過Show Package Content就可以看到整個bundle的內容.