规范之力:正确使用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的讨论

 

posted on 2012-12-12 20:09  111111***  阅读(3699)  评论(0编辑  收藏  举报