美文网首页iOS收藏学无止境恩美第二个APP项目
iOS开发笔记 | 由使用Masonry布局不能立即获取到fra

iOS开发笔记 | 由使用Masonry布局不能立即获取到fra

作者: Lol刀妹 | 来源:发表于2017-09-27 19:48 被阅读1372次
    歌手 iu

    前言

    我相信很多同学曾经都遇到过这样的问题:明明用masonry布好局了,怎么获取到的frame就是0呢?
    解决问题不难,百度一下就能找到答案,但如果只是单纯的解决问题而不去想为什么或许会制约我们的成长。

    问题重现

    1. 先看下面这段代码:

    [self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.mas_equalTo(UIEdgeInsetsMake(0, 0, 0, 0));
    }];
    NSLog(@"%@", self.scrollView);
    

    打印结果:
    <UIScrollView: 0x7fc461014000; frame = (0 0; 0 0);

    2. 再看一段代码:

    [self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.mas_equalTo(UIEdgeInsetsMake(0, 0, 0, 0));
    }];
    // 先调用superView的layoutIfNeeded方法再获取frame
    [self.view layoutIfNeeded];
    NSLog(@"%@", self.scrollView);
    

    打印结果:
    <UIScrollView: 0x7fc72d016000; frame = (0 0; 375 667);

    3. 最后看一段代码:

    [self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.mas_equalTo(UIEdgeInsetsMake(0, 0, 0, 0));
    }];
    // 0.1秒后获取frame
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"%@", self.scrollView);
    });
    

    打印结果:
    <UIScrollView: 0x7fc8cc024400; frame = (0 0; 375 667);

    问:为什么要调用superView的layoutIfNeeded或者延迟0.1秒后才能获取到frame?

    iOS高端妓术裙的裙主:锤神 告诉我:

    首先你要知道autolayout和frame的关系,autolayout最终也是转成frame,masonry是建立在autolayout之上的。你没获取到正确的值,那是因为约束还没布局完成。相当于就是我们给一定的约束,系统内部自己去根据约束条件转成对应的frame,而这需要一个过程。想要拿到正确的frame最好的就是让autolayout完成之后,什么时候完成呢?那就是在layoutsubviews for view or didlayoutsubviews for controller 里获取,当然在控制器的viewdidappear里也拿得到,但是正确做法和最佳做法还是在控制器里的viewdidlayout里获取最好~因为autolayout会根据约束,不停的去改变frame,这方法里最后拿到的frame就是最终姿势.

    锤神最后补充了一句:
    可能有些需要纠正,但这基本是我一个大概的理解~~

    看,多么谦虚~

    请裙主笑纳

    我来总结一下锤神的回答:

    约束转frame需要时间。

    good boy

    探求原因

    先不管锤神的回答,自己一步一步思考下。

    猜想:Masonry的block的回调不是立即的

    比如这段代码:

    NSLog(@"a");
    [self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
        NSLog(@"b");
        make.edges.mas_equalTo(UIEdgeInsetsMake(90, 90, 90, 90));
    }];
    NSLog(@"c");
    

    按照猜想,打印结果应该是“acb”
    但实际上却是“abc”。

    为什么?

    看看Masonry的源码就知道了:

    - (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
        self.translatesAutoresizingMaskIntoConstraints = NO;
        MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
        block(constraintMaker);
        return [constraintMaker install];
    }
    

    我们调用mas_makeConstraints这个方法的时候block就立即调用了。

    那这就更奇怪了,既然获取frame的时候已经设置好约束了,怎么获取到的frame却是0呢?

    因为你设置好了约束不代表它就转化成了frame,正如锤神所说,这需要一个过程

    0.1秒后能获取到正确的frame是因为这个时候布局已经完成。

    那么我们怎么知道布局完成的这个点?

    锤神说的是didlayoutsubviews这个方法,虽然锤神很牛逼,但我们也应该保持怀疑态度,看下官方文档验证一下锤神的说法:

    可见,锤神是个可靠的裙主。

    再来一段代码让大家加深一下印象:

    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
        
        self.scrollView = [[UIScrollView alloc] init];
        [self.view addSubview:self.scrollView];
        [self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
            NSLog(@"建立约束");
            make.edges.mas_equalTo(UIEdgeInsetsMake(90, 90, 90, 90));
        }];
        
        NSLog(@"直接获取frame:%@", self.scrollView);
        
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"0.1秒后获取frame:%@", self.scrollView);
        });
    }
    
    - (void)viewDidLayoutSubviews {
        NSLog(@"viewDidLayoutSubviews时的frame:%@", self.scrollView);
    }
    

    控制台信息:

    masonry block里的代码是最先执行的,接着是直接获取frame,然后是viewDidLayoutSubviews,最后才是0.1秒后block。

    layoutIfNeeded做了什么事?

    弄清了延迟0.1加载可以获取到正确frame的原因,再来研究下为什么调用了[xx.superView layoutIfNeeded]也能获取到正确frame的原因。

    先看一下官方文档对这个方法的描述:


    立即布局。

    我在上面的代码中多加一句[self.scrollView.superview layoutIfNeeded];:

    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
        
        self.scrollView = [[UIScrollView alloc] init];
        [self.view addSubview:self.scrollView];
        [self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
            NSLog(@"建立约束");
            make.edges.mas_equalTo(UIEdgeInsetsMake(90, 90, 90, 90));
        }];
        
        // 多加的这句 
        [self.scrollView.superview layoutIfNeeded];
        NSLog(@"直接获取frame:%@", self.scrollView);
        
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"0.1秒后获取frame:%@", self.scrollView);
        });
    }
    
    - (void)viewDidLayoutSubviews {
        NSLog(@"viewDidLayoutSubviews时的frame:%@", self.scrollView);
    }
    

    控制台信息:


    可以发现,调用[self.scrollView.superview layoutIfNeeded];时回调了viewDidLayoutSubviews方法。这就是调用[xx.superView layoutIfNeeded]后能获取到正确frame的原因。

    其他人的回答,大家可以参考下

    1. 在Stack Overflow的提问:https://stackoverflow.com/questions/46387298/why-i-cant-get-the-frame-immediately-when-i-use-masonry
    2. 在SegmentFault的提问:https://segmentfault.com/q/1010000011332667

    引申:UIButton的titleLabel的宽度为0

    之前在开发中遇到的问题,当时还不怎么理解,先看代码:

    UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(90, 90, 200, 40)];
    [self.view addSubview:button];
    [button setTitle:@"呵呵哒" forState:UIControlStateNormal];
    NSLog(@"%@", button);
    NSLog(@"%@", button.titleLabel);
    

    控制台信息:



    titleLabel的size是0。

    如果我加上这句:[button layoutIfNeeded];,titleLabel的size就是期望的了:

    参考:https://stackoverflow.com/questions/25758599/uibutton-titlelabel-frame-size-returning-cgsize-with-zero-width-height

    总结

    遇到问题,如果找不到满意的答案,可以:

    1. 主动去问答网站提问
    2. 主动虚心向大神请教
    3. 上面两招同时使用效果更佳

    最后

    赠送一把Xcode之锤beta版给锤神


    Xcode之锤-beta版

    当晚更新

    后来有个同学告诉我不需要延迟0.1秒,延迟0秒就可以获取到frame了:

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"0秒后获取frame:%@", self.scrollView);
    });
    

    是的,你没看错,延迟0秒也可以获取frame。

    我很懵逼,延迟0秒跟没延迟有区别?

    NSLog(@"直接获取frame:%@", self.scrollView);
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"0秒后获取frame:%@", self.scrollView);
    });
    

    不明白为什么0秒后获取frame可以但是直接获取却不可以。

    这位同学的说法是:

    他分享了一篇关于runloop的文章:http://www.cocoachina.com/ios/20150601/11970.html

    很显然以我现在的水平根本hold不住。

    这个问题我会一直研究,这期间肯定会逐渐学习runloop的知识,希望能早点领悟这个问题吧。

    非常欢迎大神对这个问题提出自己的看法。

    另外值得一提的是,这个同学工作也才两年左右,对于再过几个月也有两年工作经验的我来说,深感差距之大。

    此时的内心活动

    相关文章

      网友评论

      • AliceJordan:Thanks 讲的很明白 老哥加个练习方式呗 iOS小白学习的路上很是迷茫啊~
      • lizhi_boy:就冲一张图片,我居然看完了:joy: :joy:
        Lol刀妹:看来好的图片很重要
      • 滚来滚去的桔子:为什么我在didlayoutsubviews 依然得不到 frame,必须 didappear 才能拿到?
      • 超_iOS:楼主你想的真周到,我只是用而没怎么想过:cry:
        Lol刀妹:@_超 很多时候想着想着就懵逼了:joy:
      • sindri的小巢:不是下一帧才刷新,是下个runloop循环
        Lol刀妹:@sindri的小巢 感觉好专业:joy:感谢分享:grin:
        sindri的小巢:@无夜之星辰 如果你在重新设置之后想获取frame,可以用这段代码:
        dispatch_async(dispatch_get_main_queue(), ^{
        dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"%@", NSStringFromCGRect(view.frame));
        });
        });
        正常来说你async到主线程的任务会在下次runloop执行,但是也不一定,因为一次runloop实际上有两个阶段会去处理主线程的queue的任务。一次发生在beforeWaiting之前,一次在beforeWaiting之后。而UI的更新是在beforeWaiting阶段,两次嵌套是可以保证回调时UI已经更新的了。而且runloop运行的很快,两次async几乎是马上就回调的
        Lol刀妹:@sindri的小巢 :+1:
      • Sunxxxxx丶:大神。我想问下。平常写页面布局的时候在哪个位置设置view的初始frame比较好呢。因为如果在viewdidload里写。无法获取self.view的正确frame。如果在viewwillappear里写又会被频繁调用。在layoutsubviews里写也会被频繁调用。一直没有找到合适的位置设置frame。求大神指教。
        ZZZEoEv:一般VC.view的Frame确定最早是在ViewWillAppear的时候, 所以一般在这个时候设置Frame.
        ViewWillAppear一般不会被调用多次吧
        Lol刀妹:@Sunxxxxx丶 可以看下8楼的评论
        Lol刀妹:@Sunxxxxx丶 使用自动布局,最好是忘了frame,这是锤神告诉我的:joy:
      • 蜜锋将有小肚腩:毕竟锤神
        Lol刀妹:@爱吹水的蜜蜂 装备Xcode之锤的锤神6得一匹
      • CepheusSun:快师傅,锤师傅简直不要太6,就是有个不情之请:能不能慢点学,徒弟跟不上了:joy:
        Lol刀妹:@圣僧留步 你也是锤神徒弟?:sweat_smile:
        圣僧留步:锤神能不能学慢点,徒弟跟不上啦
        Lol刀妹:从来都是徒弟我跟随你和锤神师傅的脚步🙃
      • 37e4dd6ddc60:师傅就是666
        Lol刀妹:毕竟是向🔨师傅学来的:sunglasses:
      • 因幡白兔:可以可以全是细节
      • A天天涨不停:66666
        圣僧留步:锤神收新徒弟啦索,66666
        菠萝吹雪xs:锤神就是屌
        Lol刀妹:@水瓶座iOSer 🔨师傅教得好

      本文标题:iOS开发笔记 | 由使用Masonry布局不能立即获取到fra

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