iOS 正確使用UIViewController



規范之力:正確使用UIViewController(1)

標簽: uiviewcontroller

頂[0] 分享到 發表評論(0) 編輯詞條

VC的設計初衷==》一個ViewController應該且只應該管理一個view hierarchy;

目錄

一.UIViewController編輯本段回目錄

做iOS開發的經常會和UIViewController打交道,從類名可知UIViewController屬於MVC模型中的C(Controller),說的更具體點它是一個視圖控制器,管理着一個視圖(view)。
UIViewController的view是lazy loading的,當你訪問其view屬性的時候,view會從xib文件載入或者通過代碼創建(覆蓋loadView方法,自定義其view hierarchy),並返回,如果要判斷一個View Controller的view是否已經被加載需要通過其提供的isViewLoaded方法來判斷。 view加載后viewDidLoad會被調用,這里可以進行一些數據的請求或加載,用來更新你的界面。 當view將被加入view hierarchy中的時候viewWillAppear會被調用,view完成加入的時候viewDidAppear會被調用,同樣當view將要從view hierarchy中移除的時候viewWillDisappear會被調用,完成移除的時候viewDidDisappear會被調用。  當內存緊張的時候,所有的UIViewController對象的didReceiveMemoryWarning會被調用,其默認實現是 如果當前viewController的view的superview是nil的話,則將view釋放且viewDidUnload會被調用,viewDidUnload中你可以進行后繼的內存清理工作(主要是界面元素的釋放,當再次加載的時候需要重建)。
如果想要展示一個View Controller,一般有如下一種途徑
  1. 設置成Window的rootViewController(iOS 4.0之前UIWindow並沒有rootViewController屬性,只能通過addSubview的方式添加一個View Controller的view)
  2. 使用某個已經存在的Container來展示,比如使用UINavigationController來展示某個View Controller [navigationController pushViewController:vc animated:YES];
  3. 以模態界面的方式展現出來 presentModalViewController
  4. 以addSubview的方式將使其view作為另一個View Controller的view的subView

直接使用4種方法是比較危險的,上一級 View Controller並不能對當前View Controller的 生命周期相關的函數進行調用,以及旋轉事件的傳遞等。

 

二.Hierarchy編輯本段回目錄

 
我們知道一個View可以將另一個View添加為子View(subview),構成一個View Hierarchy.當某一個View添加到window的View Hierarchy中時,將被“顯示”。每一個View Controller管理着的其實就是一個View Hierarchy. 而View Controller本身可以有Child View Controller,(需要考量此話。vc中個人認為只存在前后關系,不存在父子層級關系)所以也存在一個 View Controller Hierarchy的概念,當View Controller收到上層傳來的諸如旋轉,顯示事件的時候,需要傳遞給它的Child View Controller. 一般情況下,View Hierarchy 和 View Controller Hierarchy需要保持一致性,比如一個View Controller的view的superView是由其parent view controller管理着

 

三.Container編輯本段回目錄

 
一個iOS的app很少只由一個ViewController組成,除非這個app極其簡單。 當有多個View Controller的時候,我們就需要對這些View Controller進行管理。 那些負責一個或者多個View Controller的展示並對其視圖生命周期進行管理的對象,稱之為容器,大部分容器本身也是一個View Controller,這樣的容器可以稱之為Container View Controller,也有極少數容器不是View Controller,比如UIPopoverController,其繼承於NSObject。
我們常用的容器有 UINavigationController,UITabbarController等,一般容器有一些共同的特征:

   1. 提供對Child View Controller進行管理的接口,比如添加Child View Controller,切換Child View Controller的顯示,移除Child View Controller 等
  2. 容器“擁有”所有的Child View Controller
  3. 容器需要負責 Child View Controller的appearance callback的調用(viewWillAppear,viewDidAppear,viewWillDisaapper,viewDidDisappear),以及旋轉事件的傳遞
  4. 保證view hierarchy 和 view controller hierarchy 層級關系一致,通過parent view controller將child view controller和容器進行關聯


從上面可以看出來,實現一個Container View Controller並不是一個簡單的事情,好在iPhone的界面大小有限,一般情況下一個View Controller的view都是充滿界面或者系統自帶容器的,我們無需自己創建額外的容器,但是在iPad中情況就不同了。

 

四.Custom Container View Controller編輯本段回目錄

 
在iOS 5之前框架並不支持自定義 Container View Controller,  iOS 5開始開放了一些新的接口來支持支持自定義容器
 addChildViewController:
removeFromParentViewController
transitionFromViewController:toViewController:duration:options:animations:completion:
willMoveToParentViewController:
didMoveToParentViewController:
其中前兩個接口比較重要,可以直接改變View Controller 的 Hierarchy。
有點意外的是,在不做任何額外設置的情況下進行如下操作
[viewController.viewaddSubview:otherViewController.view]
iOS 5中otherViewController是可以立刻收到viewWillAppear和viewDidAppear的調用。

至於旋轉事件的傳遞以及其他時機viewWillAppear viewDidAppear的調用是需要建立在 [viewController addChildViewController:otherViewController]基礎上的。
當我們需要在iOS 4上實現自定義容器,或者有時候我們不想讓viewWillAppear這類方法被自動調用,而是想自己來控制,這個時候我們就得需要手動來調用這些方法,而不是由框架去自動調用。(謹慎,實質上這是不規范的做法。這也是本題所言“規范之力”如何作為之)
 iOS 5中可以很方便的禁用掉自動調用的特性,覆蓋automaticallyForwardAppearanceAndRotationMethodsToChildViewControllers返回NO
但是單單覆蓋這個方法在iOS5下還是有問題的,當執行下面的語句的時候
[viewController.viewaddSubview:otherViewController.view]
otherViewController還是是可以立刻收到viewWillAppear和viewDidAppear的調用。
 解決這一問題的方法就是在iOS5的時候調用[viewController.view addSubview:otherViewController.view]之前 進行如下操作
[viewControlleraddChildViewController:otherViewController]  
(除非透徹理解了規則,不局限於規則之力的范疇。超出規則而使用。要不輕易別這么干)

總的來說實現兼容iOS 4和iOS 5的容器有不少問題和注意點的:

  1. view加入view層級前后分別調用viewWillAppear和viewDidAppear;容器的viewWillAppear,viewDidAppear,viewWillDisappear,viewDidDisappear中需要對當前顯示的Child View Controller調用相同的方法,容器需要保證Child View Controller的viewWillAppear調用之前Child View Controller的view已經load了.還有一點就是保證容器的View不會出現bounds為CGRectZero的情況,因為如果此View包含多個subview,其bounds改變的時候subview會根據自己的autoresizingMask改變frame,但是當bounds變為0再變為非0的時候,subview的frame就有可能不是你想要的了(比如某個subview的autoresizingMask為UIViewAutoresizingFlexibleBottomMargin)
  2. 容器的shouldAutorotateToInterfaceOrientation中需要檢測每一個Child View Controller的shouldAutorotateToInterfaceOrientation如果一個不支持,則看做不支持
  3. 容器的willRotateToInterfaceOrientation,didRotateFromInterfaceOrientation,willAnimateRotationToInterfaceOrientation方法中需要將這些事件傳遞給所有的Child View Controller
  4. 由於UIViewController的parentViewController屬性為只讀,且iOS4中沒有提供容器支持的接口(iOS 5中容器支持的接口會間接的維護這個屬性),所以為了使得childViewController和容器得以關聯,我們可以頂一個View Controller的基類,添加一個比如叫做superController的屬性用來指定對應的parentViewController
  5. 由於UIViewController的interfaceOrientation為只讀屬性,且iOS5中沒有提供容器接口,所以UIViewController的這個interfaceOrientation變的不可信,為了取得當前UIViewController的orientation我們可以用UIWindow下的rootViewController的interfaceOrientation的值
  6. 容器的viewDidUnload方法中需要對view未釋放的childViewController的view進行釋放,且調用其viewDidUnload方法
蘋果對UIViewController以及其使用有着非常詳細的文檔 UIViewController Reference , ViewController Programming Guide。

一個ViewController應該且只應該管理一個view hierarchy

 UIViewController的誤用

什么是UIViewController的誤用?
UIViewController是iOS開發中最常見也最重要的部件之一,可以說絕大多數的app都用到了UIViewController來管理頁面的view。它是MVC的核心結構和橋梁構成,可以說UIViewController是絕大多數開發者所花時間最多的部分了。
但是正是這樣一個重要的類卻經常被誤用,從而導致app的不穩定,莫名奇妙的bug甚至無法通過appstore的審查。最常見和最可怕的誤用在於在一個UIViewController里加入本來不應該由它管理的其他UIViewController,也即違反了Apple在開發者文檔中關於UIViewController的描述:

 Each custom view controller object you create is responsible for managing all of the views in a single view hierarchy. In iPhone applications, the views in a view hierarchy traditionally cover the entire screen, but in iPad applications they may cover only a portion of the screen. The one-to-one correspondence between a view controller and the views in its view hierarchy is the key design consideration. You should not use multiple custom view controllers to manage different portions of the same view hierarchy. Similarly, you should not use a single custom view controller object to manage multiple screens worth of content.
一個ViewController應該且只應該管理一個view hierarchy ,而通常來說一個完整的view hierarchy指的是整整占滿的一個屏幕。而很多app滿屏中會有各個區域分管不同的功能,一些開發者喜歡直接新建一個ViewController和一套相應的View來完成所要的功能(比如我自己=_=)。雖然十分方便,但是卻面臨很多風險..
一般來說,只要你的代碼中含有類似這樣的語句,那你一定是誤用UIViewController了
 
 
viewController.view.bounds=CGRectMake(50,50,100,200);
[viewController.viewaddSubview:someOtherViewController.view];



這樣的代碼可能導致莫名的bug,也會令接手的開發者無所適從。
小題大做吧,這樣用會有什么問題呢一個很麻煩的問題是,這將會導致你的app在不同的iOS版本上有不同的表現。在iOS5之前,能夠對viewController進行管理的類有UINavigationController,UITabBarController和iPad專有的UISplitViewController。而在iOS5中加入了可自定義的ViewControllers的容器。由於新的SDK的處理機制,iOS4前通過addSubview加到當前controller的view上的view的呈現,將不會觸發被加入view hierarchy的view的controller的viewWillAppear:方法。而且,新加入的viewController也不會接收到諸如didReceiveMemoryWarning等委托方法,而且也不能響應所有的旋轉事件!而iOS5中由於所謂的custom container VC的出現,上述方法又能夠運行良好,這導致了同樣代碼在不同終端產生不同的行為,為之后的維護和進一步開發埋下了隱患。另外,用這樣的方法所添加的viewController顯然違背了Apple的本意,它的parentViewController,interfaceOrientation顯然都是錯誤的,有時候甚至會出現觸摸事件無法響應等嚴重問題。
好吧,那我們要怎么辦如果你已經在一個app里這樣誤用了大量的viewController,那可能的辦法也許是盡力去自行處理各種非正常的狀況,比如在addSubview之后手動調用加入的vc的viewWillAppear:,以及在收到didReceiveMemoryWarning后順次調用子VC的didReceiveMemoryWarning(顯然都是很蛋疼的做法啊)。但是需要注意的是iOS5中這些方法的調用似乎是沒有問題的(至少我測試是這樣),因此需要對不同版本系統進行分別處理。可以用UIDevice的方法確定運行環境的系統版本:
 
 // System Versioning Preprocessor Macros

#define SYSTEM_VERSION_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedSame)
#define SYSTEM_VERSION_GREATER_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedDescending)
#define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending)
#define SYSTEM_VERSION_LESS_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedAscending)
#define SYSTEM_VERSION_LESS_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedDescending)

在合適的時機判定判定系統版本,手動調用對應方法:
 
 if(SYSTEM_VERSION_LESS_THAN(@“5.0”))
{
//viewWillAppear或didReceiveMemoryWarning或其他
}
顯然,這樣的代碼既非優雅亦難維護,而且隨着iOS版本的更新,誰也不知道這段代碼之后會不會有什么問題,無形中增加了開發成本。
真正的解決之道當然是嚴格遵守Apple提供的設計規范,每個VC管理一個view hierarchy。在設計的時候,永遠記住你的view和view controller都需要重用,而不恰當的使用view controller會導致重用性大打折扣。而通用的view有時也需要一個類似controller的東西來管理它的subview的行為,或者做出某些相應,這個時候我們不妨想一想一些Apple寫的經典的view是如何實現的,比如UITableView和UIPickerView,依靠protocol的各種方法進行配置。
作為自定義的view的controller應當是直接繼承自NSObject的類,而不應該是UIViewController。一個UIViewController可以包含若干個這樣的controller來控制一個view中的不同部分的功能實現,而對於對應的自定義view是代碼寫的還是nib出來的就無所謂了。當然,如果是新接觸iOS開發的話,我個人不建議使用Interface Builder,除非你確實清楚IB到底在背后為你做了什么。在當你完全清楚之后,IB確實能極大提升開發效率(特別是在Xcode4以后),但是如果你的對IB和view加載連接的概念如同毛線團的話,IB的使用只會讓你以及讓你的同事茫然失措。
在iOS5中提供了所謂的container of View Controllers,有興趣的童鞋可以參看WWDC 2011的Session 102 – Implementing UIViewController Containment(需要一個野生開發者賬號)
一些資料作為iOS開發者,Apple的關於UIViewController的文檔以及開發者的一些討論是必讀的,簡單整理如下:

   * View Controller Programming Guide for iOS
   * 關於誤用UIViewController而造成的私有API調用的討論
   * stack overflow上關於誤用view controller的討論

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM