深入了解控制器View的加载

作者: 喵子G | 来源:发表于2017-03-16 12:56 被阅读967次

    一,非storyboard下的控制器View加载

    1,加载过程中的方法调用顺序

    - (instancetype)init;
    // 纯代码的控制器也会走这个方法
    - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil;
    - (void)loadView;
    - (void)viewDidLoad;
    - (void)viewWillAppear:(BOOL)animated;
    - (void)viewDidAppear:(BOOL)animated;
    

    无论是有xib关联的控制器还是纯代码的控制器,都会走完这些方法。

    2,view加载的规律

    1:有xib的控制器,加载他的view的时候,在viewWillAppear方法的时候,self.view的frame才是对应屏幕的尺寸,在它之前的方法中self.view的尺寸都是xib的view的尺寸。即有xib的控制器,viewDidLoad中xib的view的frame是不准确的。(没有xib的纯代码的控制器,self.view.frame是准确的)
    2:有xib的控制器,重写loadView方法会使控制器不能加载到xib的view,如果一定要重写loadView方法,那么控制器的初始化就要用initWithNibName方法:[[JKRViewController alloc] initWithNibName:@"JKRViewController" bundle:nil];这样就能同时使用xib,还能重写loadView方法。但是这样的话,init方法就不会被调用。
    3:如果重写loadView方法,一定要给self.view赋值,要么就写上[super loadView];要么就自己给self.view赋值,否则会死循环。
    4:如果纯代码的控制器,将self.view替换成xib的view,那么在viewWillAppear之前,self.view.frame的尺寸也是不准确的,同样是xib的尺寸。
    5:这里完全不考虑有xib的控制器,将self.view替换成有xib文件对于的view这个情况,因为如果要将控制器的view替换成其他view,这个控制器完全没有必要还对应一个xib文件,除了增加bug几率和冗余文件没有任何用处。

    3,实用方法

    1,xib下让viewDidLoad中的self.view.frame是准确的

    因为loadView方法在viewDidLoad方法之前调用,可以在这里设置self.view.frame等于屏幕的尺寸:

    - (void)loadView {
        [super loadView];
        self.view.frame = [UIScreen mainScreen].bounds;
    }
    

    这样在viewDidLoad中self.view.frame的尺寸就是正确的了,同时别忘记控制器的初始化一定要用initWithNibName方法。

    2,纯代码的控制器将self.view替换成xib的view,让viewDidLoad中的self.view.frame是准确的

    - (void)loadView {
        self.view = [[NSBundle mainBundle] loadNibNamed:@"JKRRootView" owner:nil options:nil].firstObject;
        self.view.frame = [UIScreen mainScreen].bounds;
    }
    

    3,开发中注意事项

    1,带xib文件的控制器,viewDidLoad中的self.view.frame是不准确的

    所有如果控制器xib中view尺寸是320 * 480(iphone4S),在viewDidLoad中添加如下代码:

    - (void)viewDidLoad {
        [super viewDidLoad];
        UIView *view = [[UIView alloc] initWithFrame:self.view.bounds];
        view.backgroundColor = [UIColor redColor];
        [self.view addSubview:view];
    }
    

    在7S下运行实际效果:


    QQ20170316-124356.png

    解决1:当然可以重写loadView方法

    - (void)loadView {
        [super loadView];
        self.view.frame = [UIScreen mainScreen].bounds;
    }
    

    让self.view.frame在viewDidLoad中是准确的,对于经常用纯代码写控制器的人来讲,这样可能会更舒服些,但是控制器初始化要用initWithNibName方法。

    解决2:这样写是(可能)是最好的:

    - (void)viewDidLoad {
        [super viewDidLoad];
        [self.view addSubview:self.subView];
    }
    
    - (void)viewWillAppear:(BOOL)animated {
        [super viewWillAppear:animated];
        self.subView.frame = self.view.bounds;
    }
    
    - (UIView *)subView {
        if (!_subView) {
            _subView = [[UIView alloc] init];
            _subView.backgroundColor = [UIColor redColor];
        }
        return _subView;
    }
    

    思考1:关于loadView,网上有的说法是永远不重写这个方法,但是在这种情况下,重写了loadView确实是能够解决问题,至于到底如果做更好,现在还不确定。
    思考2:关于subView的frame是在viewDidLoad设置好还是在viewWillAppear中设置好的问题,网上有的说法是,控制器viewDidLoad在控制器的一个生命周期只会调用一次,而viewWillAppear会调用多次,所有viewDidLoad更适合做一次性初始化操作,而不适合做frame操作。我个人觉得,viewDidLoad,做初frame定义反而更好,因为尺寸的定义,大多数情况下,是没有必要重复确定的。

    2,带xib文件的控制器,viewDidLoad中的self.view.frame是不准确的,但是在viewDidLoad中设置subView的约束是准确的

    代码如下:

    - (void)viewDidLoad {
        [super viewDidLoad];
        [self.view addSubview:self.subView];
        self.subView.translatesAutoresizingMaskIntoConstraints = NO;
        NSLayoutConstraint *top = [NSLayoutConstraint constraintWithItem:self.subView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:0];
        NSLayoutConstraint *leading = [NSLayoutConstraint constraintWithItem:self.subView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeading multiplier:1 constant:0];
        NSLayoutConstraint *bottom = [NSLayoutConstraint constraintWithItem:self.subView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeBottom multiplier:1 constant:0];
        NSLayoutConstraint *trailing = [NSLayoutConstraint constraintWithItem:self.subView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTrailing multiplier:1 constant:0];
        [self.view addConstraint:top];
        [self.view addConstraint:leading];
        [self.view addConstraint:bottom];
        [self.view addConstraint:trailing];
    //    设置frame是不准确的,因为现在self.view.bounds等于xib中view的尺寸
    //    self.subView.frame = self.view.bounds;
    }
    

    3,纯代码的控制器,将self.view替换成有xib文件对应的view,viewDidLoad中,情况和带xib文件的控制器一样

    viewDidLoad中frame尺寸是不准确的
    viewDidLoad中设置约束是准确的
    可以通过重写loadView方法,修正self.view的frame来让viewDidLoad的尺寸正确
    在viewWillAppear中self.view的frame是准确的

    4,其他

    一直没有提到纯代码控制器,将self.view替换成纯代码的view这种情况,这种情况下,self.view.frame在viewDidLoad中是准确的,和纯代码控制器没有区别。

    4,开发常遇到的问题和一些技巧

    1,xib文件的命名

    如果控制器文件叫JKRViewController.h,且没有对应的JKRViewController.xib文件情况下,创建xib的view创建不要起的名字和控制器的名字或控制器的名字头一样,即xib的view文件名不能叫JKRView.xib。

    2, 是否使用storyboard环境的配置

    不用storyboard的情况下,记得删除Main Interface,否则会出现同时两个UIWindow的情况,反之,用storyboard做主窗口的时候,记得添加上。

    3,为纯代码控制添加关联的xib文件

    为纯代码控制器关联一个xib文件时,除了创建一个和控制器文件名前缀相同的xib文件,别忘记在xib中在File’s Owner中CustomClass定义成关联控制器,并把xib的view的Referencing Outlets关联到File’ Owner的view上,否则不会识别。

    4,xib可视化

    自定义有xib关联的控件的时候,可以实现可视化的效果:

    QQ20170316-125057.png

    实现步骤如下:
    1,定义UIView的可视化属性

    IB_DESIGNABLE
    @interface DemoView : UIView
    
    @property (nonatomic, strong) IBInspectable UIColor *circleColor;
    
    @end
    

    2,定义UIView的绘制:

    @implementation DemoView
    
    - (void)drawRect:(CGRect)rect {
        [super drawRect:rect];
        CGContextRef context = UIGraphicsGetCurrentContext();
        UIBezierPath *path = [UIBezierPath bezierPath];
        CGPoint center = CGPointMake(rect.size.width * 0.5, rect.size.height * 0.5);
        CGFloat radius = rect.size.width * 0.5;
        [path addArcWithCenter:center radius:radius startAngle:0 endAngle:M_PI * 2 clockwise:YES];
        CGContextSetLineWidth(context, 2);
        [_circleColor setStroke];
        CGContextAddPath(context, path.CGPath);
        CGContextStrokePath(context);
    }
    
    @end
    

    二,storyboard下的控制器View加载

    1,加载过程中的方法调用顺序

    网上有说storyboard下控制器加载会走init / initWithNibName方法,我实际测试,是没有走这两个方法的,但是一定走awakeFromNib方法(纯代码和xib关联的控制器加载都不会走这个方法)。
    测试顺序如下:
    awakeFromNib
    loadView
    viewDidLoad
    viewWillAppear

    2,view的尺寸问题

    测试storyboard下的控制器,viewDidLoad中的self.view.frame是准确的,不论storyboard中控制器的尺寸如何,所以可以大胆的在这里用self.view.bounds,self.view.frame来定义尺寸。
    测试下awakeFromNib中控制器的view尺寸也是准确的。

    思考:我认为不应该在这里写初始化或者尺寸操作的代码。

    首先,awakeFromNib是在loadView之前调用的,虽然这里会完成view的初始化和调用,但是并不大符合逻辑。
    其次,因为xib和纯代码的控制器都不会走这个方法,频繁更换初始化和尺寸操作代码的位置,造成不少记忆成本,也不利于个人代码风格的统一,最好xib,纯代码,storyboard下的相关代码都在一个位置写,这样统一自己的代码书写规范。

    3,storyboard替换控制器的view

    storyboard下的控制器,如果重写loadView方法,替换self.view为xib关联的UIView,那么效果就和纯代码和xib关联的view是一样的了,即:
    viewDidLoad中frame尺寸是不准确的
    viewDidLoad中设置约束是准确的
    可以通过重写loadView方法,修正self.view的frame来让viewDidLoad的尺寸正确
    在viewWillAppear中self.view的frame是准确的

    思考:开发中的应用

    这个应该是实际开发中用的到的地方,比如storyboard树中的某个控制器界面突然因为某些原因需要需要添加另一个种界面状态,就可以用xib新建一个界面,然后在控制器loadView方法把它替换上去,但是至于之后的跳转问题,还是要考虑的。如图:

    QQ20170316-125538.png

    根控制器默认点击按钮跳转灰色控制器,蓝色的UIView就是替换控制器self.view的UIView,按钮蓝色界面按钮跳转到黄色控制器。
    实现方法:
    1,控制器内拖一个UIView
    2,把UIView拖到控制器顶部
    3,把UIView关联到控制器属性上
    4,重写loadView方法:

    - (void)loadView {
        if (0) { //满足某种条件替换默认界面
            self.rootView3.userInteractionEnabled = YES;
            self.view = self.rootView3;
        } else {
            [super loadView];
        }
    }
    

    因为我平时很少用到storyboard,能想到的只有这些了。

    获取授权

    相关文章

      网友评论

        本文标题:深入了解控制器View的加载

        本文链接:https://www.haomeiwen.com/subject/bovdnttx.html