一般而言,我們都會在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吧。