iOS App开发中UIViewController类的使用教程

时间:2021-12-18 02:48:58

一、引言
作为mvc设计模式中的c,controller一直扮演着项目开发中最重要的角色,它是视图和数据的桥梁,通过它的管理,将数据有条有理的展示在我们的view层上。ios中的uiviewcontroller是uikit框架中最基本的一个类。从第一个ui视图到复杂完整项目,都离不开uiviewcontroller作为基础。基于uiviewcontroller的封装和扩展,也能够出色的完成各种复杂界面逻辑。这里旨在讨论uiviewcontroller的生命周期和属性方法,在最基础的东西上,往往会得到意想不到的惊喜。

二、uiviewcontroller的生命周期
要了解uiviewcontroller,先要弄清楚其生命周期。在面向对象的语言中,是对象,就一定要有生命周期,uiviewcontroller也不例外,生命周期管理controller的作用范围和时间,也管理其内对象的作用范围和时间。首先,uiviewcontroller中与其生命周期有关的几个函数如下:
//类的初始化方法
+ (void)initialize;
//对象初始化方法
- (instancetype)init;
//从归档初始化
- (instancetype)initwithcoder:(nscoder *)coder;
//加载视图
-(void)loadview;
//将要加载视图
- (void)viewdidload;
//将要布局子视图
-(void)viewwilllayoutsubviews;
//已经布局子视图
-(void)viewdidlayoutsubviews;
//内存警告
- (void)didreceivememorywarning;
//已经展示
-(void)viewdidappear:(bool)animated;
//将要展示
-(void)viewwillappear:(bool)animated;
//将要消失
-(void)viewwilldisappear:(bool)animated;
//已经消失
-(void)viewdiddisappear:(bool)animated;
//被释放
-(void)dealloc;

上面这么多的函数,乍一看什么复杂,其实关系什么明朗,除了initialize,init和initwithcoder不是存在所有对象的声明周期中,其他函数都会在uiviewcontroller的声明周期中有序的被调用。那么具体的调用顺序是怎样的呢,最好的办法是实践一下,通过编号打印,结果如下:

iOS App开发中UIViewController类的使用教程

这是一个viewcontroller完整的声明周期,其实里面还有好多地方需要我们注意一下:
1:initialize函数并不会每次创建对象都调用,只有在这个类第一次创建对象时才会调用,做一些类的准备工作,再次创建这个类的对象,initalize方法将不会被调用,对于这个类的子类,如果实现了initialize方法,在这个子类第一次创建对象时会调用自己的initalize方法,之后不会调用,如果没有实现,那么它的父类将替它再次调用一下自己的initialize方法,以后创建也都不会再调用。因此,如果我们有一些和这个相关的全局变量,可以在这里进行初始化。
2:init方法和initcoder方法相似,只是被调用的环境不一样,如果用代码进行初始化,会调用init,从nib文件或者归档进行初始化,会调用initcoder。
3:loadview方法是开始加载视图的起始方法,除非手动调用,否则在viewcontroller的生命周期中没特殊情况只会被调用一次。
4:viewdidload方法是我们最常用的方法的,类中成员对象和变量的初始化我们都会放在这个方法中,在类创建后,无论视图的展现或消失,这个方法也是只会在将要布局时调用一次。
5:viewwillappear:视图将要展现时会调用。
6:viewwilllayoutsubviews:在viewwillappear后调用,将要对子视图进行布局。
7:viewdidlayoutsubviews:已经布局完成子视图。
8:viewdidappare:视图完成显示时调用。
9:viewwilldisappear:视图将要消失时调用。
10:viewdiddisappear:视图已经消失时调用。
11:dealloc:controller被释放时调用。
注意:经过测试,从nib文件加载的controller,只要不释放,在每次viewwillappare时都会调用layoutsubviews方法,有时甚至会在viewdidappare后在调用一次layoutsubviews,而重点是从代码加载的则只会在开始调用一次,之后都不会,所以注意,在layoutsubviews中写相关的布局代码十分危险。

三、从storyboard加载uiviewcontroller实例的传值陷阱
我们知道,当我们从storyboard中加载viewcontroller时,我们在controller中拖拽的视图是可以被初始化的,这里面有一点需要我们注意,如果我们需要向controller中视图进行传值设置,通过以下方法得到的controller中,视图还没有被初始化创建出来:
viewcontroller2 * viewcontroller2 = [[uistoryboard storyboardwithname:@"main" bundle:[nsbundle mainbundle]] instantiateviewcontrollerwithidentifier:@"viewcontroller2"];
我们可以在viewcontroller2的storyboard中拉一个label,然后关联到头文件中,如下打印,会发现我们得到controller时,里面的视图对象并没有进行创建:
viewcontroller2 * viewcontroller2 = [[uistoryboard storyboardwithname:@"main" bundle:[nsbundle mainbundle]] instantiateviewcontrollerwithidentifier:@"viewcontroller2"];
    nslog(@"%@",viewcontroller2.label);
    [self presentviewcontroller:viewcontroller2 animated:yes completion:nil];

打印如下:

iOS App开发中UIViewController类的使用教程

可以想象,如果我们这时候需要对label进行一些属性设置,必然失败。有人提出可以在创建后,手动调以下loadview方法,我们试一下,结果如下:

iOS App开发中UIViewController类的使用教程

可以看到,手动调用loadview后,label是被创建了出来,但是暴漏了一个更严重的问题,系统不在调用viewdidload方法,这是十分有风险的,因为我们大部分的初始化代码都会放在这个方法里,所以手动调用loadview是一种错误的方法,apple文档声明对于loadview方法,我们从来都不要手动直接调用,那么我们如何实现创建后对成员对象进行传值设置呢,ios9中增加了这样一个方法:
- (void)loadviewifneeded ns_available_ios(9_0);
这个方法十分有用,调用这个方法,会将视图创建出来,并且不会忽略viewdidload的调用。
在ios9中,uiviewcontroller还增加了下面一个布尔值的属性,可以同来判断controller的view是否已经加载完成:
@property(nullable, nonatomic, readonly, strong) uiview *viewifloaded ns_available_ios(9_0);

 

四、uiviewcontroller与stroyboard的相关相互方法
对于viewconroller,我们一般有两种方式创建,一种是用纯代码的方式,一种是与storyboard关联,在uiviewcontroller中,有许多方法方便我们与storyboard进行交互联系。
1、viewcontroller直接在storyboard中进行跳转的传值
在storyboard中进行界面跳转是十分方便的,我们在storyboard中拉入两个viewcontroller,在一个上面添加一个按钮,点住按钮按住control,将鼠标拉到第二个controller上,会出现如下的跳转选项:

iOS App开发中UIViewController类的使用教程

我们选择一个后,就会在两个controller之间建立一个跳转连接。当我们运行点击按钮后,会自动从第一个controller跳转到第二个controller。在uiviewcontroller中有如下方法可以对是否跳转进行控制:
- (bool)shouldperformseguewithidentifier:(nsstring *)identifier sender:(nullable id)sender ns_available_ios(6_0);
这个方法如果返回no,自动跳转将不能进行,会被拒绝,需要注意的是,这个方法只会在自动的跳转时被调用,我们手动使用代码跳转storyboard中的连接关系时是不会被调用的,我们后面讨论。
在执行过上述方法后,如果返回yes,系统还会在执行如下一个方法,作为跳转前的准备,我们可以在这个方法中进行一些传值操作,这个方法无论使我们手动进行跳转还是storyboard中自动跳转,都会被执行:
- (void)prepareforsegue:(uistoryboardsegue *)segue sender:(nullable id)sender ns_available_ios(5_0);
sugur对象中封装了相关的viewcontroller,可以使用segue.destinationviewcontroller获取。
segue在storyboard中除了用来自动正向跳转外,我们还可以进行反向的跳转,类似pop和dismiss方法,这种segue被称为unwind sugue。例如,我们有一个controller1和一个controllert2,要使用unwind segue从2返回1,我们需要在2中实现如下格式的方法:
- (ibaction)unwindseguetoviewcontroller:(uistoryboardsegue *)segue {
    nslog(@"unwindseguetoviewcontroller");
}

这个方法中的返回值必须为ibaction,参数必须是uistoryboardsegue,方法名我们可以自己定义,之后在storyboard中的viewcontroller1中的exit选项中,我们会发现多了一个这样的方法:

iOS App开发中UIViewController类的使用教程

我们可以把它连接到viewcontroller2中的一个按钮上:

iOS App开发中UIViewController类的使用教程

这样,当我们点击viewcontroller2中的按钮时,就会返回到我们第一个viewcontroller1中了。
当然,在使用unwind segue方法时,也是会有一些回调帮助我们进行跳转前的设置和传值,uiviewcontroller如下方法会在跳转前调用,返回no,则不能进行跳转:
-(bool)canperformunwindsegueaction:(sel)action fromviewcontroller:(uiviewcontroller *)fromviewcontroller withsender:(id)sender{
    nslog(@"canperformunwindsegueaction");
    return yes;
}

之后会执行我们自定义的unwindsegue方法,这个方法中我们可以什么都不写,模式是会进行跳转的。
2、使用代码跳转storyboard中的controller
我们除了在storyboard中拉拉扯扯可以进行控制器的跳转外,我们也可以使用代码来跳转storyboard中segue连接关系。
在storyboard中两个控制器间建立一个segue联系,我们可以取一个名字:

iOS App开发中UIViewController类的使用教程

在触发跳转的方法中,使用如下方法进行跳转,这里面的参数id就是我们取得segue的id:
- (void)performseguewithidentifier:(nsstring *)identifier sender:(nullable id)sender ns_available_ios(5_0);
下面三个属性我们可以获取controller的nib文件名,其storyboard和其bundle:
@property(nullable, nonatomic, readonly, copy) nsstring *nibname; 
@property(nullable, nonatomic, readonly, strong) nsbundle *nibbundle;
@property(nullable, nonatomic, readonly, strong) uistoryboard *storyboard ns_available_ios(5_0);

 

五、uiviewcontroller之间的一些从属关系
这部分的内容和方法可能我们接触用到的并不多,但是在某些情况下,使用这些方法可以大大的方便某些逻辑。
1、parentviewcontroller
 uiviewcontroller里面封装了一个数组,可以存放其子viewcontroller,系统中使用的例子就是导航和tabbar这类的控制器,我们使用如下方法可以直接访问这些父的controller:
@property(nullable,nonatomic,weak,readonly) uiviewcontroller *parentviewcontroller;
2、模态跳转中controller的从属
在我们进行控制器的跳转时,只要控制器没有被释放,我们都可以顺藤摸瓜的找到它,使用如下两个方法:
//其所present的contller,比如,a和b两个controller,a跳转到b,那么a的presentedviewcontroller就是b
@property(nullable, nonatomic,readonly) uiviewcontroller *presentedviewcontroller  ns_available_ios(5_0);
//和上面的方法刚好相反,比如,a和b两个controller,a跳转到b,那么b的presentingviewcontroller就是a
@property(nullable, nonatomic,readonly) uiviewcontroller *presentingviewcontroller ns_available_ios(5_0);

了解了上面方法我们可以知道,对于反向传值这样的问题,我们根本不需要代理,block,通知等这样的复杂手段,只需要获取跳转到它的controller,直接设置即可。举个例子,我们需要在第二个界面消失后,改变第一个界面的颜色,在第二个controller中只需要下面的代码即可实现 :
    self.presentingviewcontroller.view.backgroundcolor = [uicolor colorwithred:arc4random()%255/255.0 green:arc4random()%255/255.0 blue:arc4random()%255/255.0 alpha:1];
    [self dismissviewcontrolleranimated:yes completion:nil];

六、uiviewcontroller的模态跳转及动画特效
单纯的uiviewcontroller中,我们使用最多的是如下的两个方法,一个向前跳转,一个向后返回:
- (void)presentviewcontroller:(uiviewcontroller *)viewcontrollertopresent animated: (bool)flag completion:(void (^ __nullable)(void))completion ns_available_ios(5_0);
- (void)dismissviewcontrolleranimated: (bool)flag completion: (void (^ __nullable)(void))completion ns_available_ios(5_0);

从方法中,我们可以看到,有animated这个参数,来选择是否有动画特效,默认的动画特效是像抽屉一样从手机屏幕的下方向上弹起,当然,这个效果我们可以进行设置,uiviewcontroller有如下一个属性来设置动画特效:
@property(nonatomic,assign) uimodaltransitionstyle modaltransitionstyle ns_available_ios(3_0);
注意,这个要设置的是将要跳转到的controller,枚举如下:
typedef ns_enum(nsinteger, uimodaltransitionstyle) {
    uimodaltransitionstylecoververtical = 0,//默认的,从下向上覆盖
    uimodaltransitionstylefliphorizontal ,//水平翻转
    uimodaltransitionstylecrossdissolve,//溶解
    uimodaltransitionstylepartialcurl ,从下向上翻页
};

除了跳转的效果,还有一个属性可以设置弹出的controler的填充效果,但是这个属性只在pad上有效,在iphone上无效,都是填充到整个屏幕:
@property(nonatomic,assign) uimodalpresentationstyle modalpresentationstyle ns_available_ios(3_2);
//枚举如下
typedef ns_enum(nsinteger, uimodalpresentationstyle) {
        uimodalpresentationfullscreen = 0,//填充整个屏幕
        uimodalpresentationpagesheet,//留下状态栏
        uimodalpresentationformsheet,//四周留下变暗的空白
        uimodalpresentationcurrentcontext ,//和跳转到它的控制器保持一致
        uimodalpresentationcustom ns_enum_available_ios(7_0),//自定义
        uimodalpresentationoverfullscreen ns_enum_available_ios(8_0),
        uimodalpresentationovercurrentcontext ns_enum_available_ios(8_0),
        uimodalpresentationpopover ns_enum_available_ios(8_0) __tvos_prohibited,
        uimodalpresentationnone ns_enum_available_ios(7_0) = -1,        
};

 

七、viewcontroller显示(关闭)其它viewcontroller
在5.0之前,对应的方法是使用model view controller系列的方法。5.0以后增加了presented,prensentingviewcontroller的概念,modalviewcontroller对应5.0后的presentedviewcontroller

fcdemoviewcontroller *controller= [[fcdemoviewcontroller alloc]initwithnibname:@"fcdemoviewcontroller" bundle:nil]; 
controller.modaltransitionstyle=uimodaltransitionstylefliphorizontal; 
[self presentmodalviewcontroller:controller animated:yes]; 
nslog(@"%@",self.modalviewcontroller); 
//5.0  
[self presentviewcontroller:controller animated:yes completion:nil]; 
nslog(@"%@",self.presentedviewcontroller); 
nslog(@"%@",self.presentingviewcontroller);  

 属性modalpresentationstyle在ipad下有几种显示形式,对应不同的显示区域。属性modelltransitionstyle决定过渡形式.
关闭也有对应的方法:dismissmodalviewcontrolleranimated或ios5.0以后的dismissviewcontrolleranimated:completion:
5.0以后其它的变化:
controller可以自定义子controller的集合,这样每一个controller都可以是一个container controller.
definespresentationcontext决定当前显示其它controller的controller是否提供context给presentedviewcontroller(modalviewcontroller),如果不,就会找上一级的controller的该值。
详细的操作可以查看class reference.
苹果官方推荐使用协议的方式来让controller相互通信。首先声明一个协议,并在主controller中实现该协议的方法,在显示其它controller的时候,为其设置delegate=self.这样在其它controller需要回调presentingviewcontroller就可以直接用delegate方法来回调到它。通过这样的方式,可以使得复用性大大增强。而且被显示的controller也不用完全知道显示它的controller的所有信息和属性。