前言
我相信很多同学曾经都遇到过这样的问题:明明用masonry布好局了,怎么获取到的frame就是0呢?
解决问题不难,百度一下就能找到答案,但如果只是单纯的解决问题而不去想为什么或许会制约我们的成长。
问题重现
在这里我还是要推荐下我自己建的iOS开发学习群:680565220,群里都是学ios开发的,如果你正在学习ios ,小编欢迎你加入,今天分享的这个案例已经上传到群文件,大家都是软件开发党,不定期分享干货(只有iOS软件开发相关的),包括我自己整理的一份2018最新的iOS进阶资料和高级开发教程
1. 先看下面这段代码:
[self.scrollViewmas_makeConstraints:^(MASConstraintMaker*make) { make.edges.mas_equalTo(UIEdgeInsetsMake(0,0,0,0));}];NSLog(@"%@", self.scrollView);
打印结果:
2. 再看一段代码:
[self.scrollViewmas_makeConstraints:^(MASConstraintMaker*make) { make.edges.mas_equalTo(UIEdgeInsetsMake(0,0,0,0));}];// 先调用superView的layoutIfNeeded方法再获取frame[self.viewlayoutIfNeeded];NSLog(@"%@", self.scrollView);
打印结果:
3. 最后看一段代码:
[self.scrollViewmas_makeConstraints:^(MASConstraintMaker*make) { make.edges.mas_equalTo(UIEdgeInsetsMake(0,0,0,0));}];//0.1秒后获取framedispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1* NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSLog(@"%@", self.scrollView);});
打印结果:
问:为什么要调用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需要时间。
探求原因
先不管锤神的回答,自己一步一步思考下。
猜想: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 { [superviewDidLoad];// Do any additional setup after loading the view, typically from a nib.self.scrollView = [[UIScrollViewalloc] 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 { [superviewDidLoad];// Do any additional setup after loading the view, typically from a nib.self.scrollView = [[UIScrollViewalloc] 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的原因。
其他人的回答,大家可以参考下
在Stack Overflow的提问: https://stackoverflow.com/questions/46387298/why-i-cant-get-the-frame-immediately-when-i-use-masonry
在SegmentFault的提问: https://segmentfault.com/q/1010000011332667
引申:UIButton的titleLabel的宽度为0
之前在开发中遇到的问题,当时还不怎么理解,先看代码:
UIButton*button = [[UIButtonalloc] 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
总结
遇到问题,如果找不到满意的答案,可以:
主动去问答网站提问
主动虚心向大神请教
上面两招同时使用效果更佳
最后
赠送一把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的知识,希望能早点领悟这个问题吧。
非常欢迎大神对这个问题提出自己的看法。
另外值得一提的是,这个同学工作也才两年左右,对于再过几个月也有两年工作经验的我来说,深感差距之大。