因为使用简单,autoresizing的自动布局方式一直挺受我偏爱的。然而前端时间测试的时候,出现了几个布局上的bug,调试后发现原来是没有用正确姿势使用autoresizing所致的(╥﹏╥)。
为了描述一下这个bug,假装我写了这样一段代码:
CGRect parentFrame = CGRectMake(0, 0, 100, 100);
UIView *parent = [[UIView alloc] initWithFrame:parentFrame];
parent.backgroundColor = [UIColor redColor];
[self.view addSubview:parent];
UIView *son = [[UIView alloc] initWithFrame:CGRectMake(0, 50, 100, 50)];
son.backgroundColor = [UIColor blueColor];
son.autoresizingMask = UIViewAutoresizingFlexibleHeight;
[parent addSubview:son];
[parent setFrame:CGRectMake(0, 0, 100, 0)];
[parent setFrame:parentFrame];
大意是,希望son这个视图的底部永远和parent视图的底部对齐,而son视图的顶部永远和parent视图的顶部有50单位的距离。也就是这样的结果:

但是运行完这段代码后我得到的结果是这样的:

于是bug就产生了(╥﹏╥)
为什么会这样呢?明明正确设置了autoresizing参数,可最后得到了错误的布局结果。仔细一想,原来是因为其中经历了
[parent setFrame:CGRectZero];
这个步骤。
在我的理解中,如果仅有UIViewAutoresizingFlexibleHeight,则表示,无论parent视图的尺寸如何变化,son视图的上边界和parent视图的上边界的距离不变,son视图的下边界和parent视图的下边界的距离不变,写成公式可能就是:
CGRectGetMinY(parent.bounds) - CGRectGetMinY(son.frame) 不变
CGRectGetMaxY(parent.bounds) - CGRectGetMaxY(son.frame) 不变
最开始,parent视图的bounds为(0, 0, 100, 100),而son视图的frame为(0, 50, 100, 50),也就是说:
CGRectGetMinY(parent.bounds) - CGRectGetMinY(son.frame) = -50
CGRectGetMaxY(parent.bounds) - CGRectGetMaxY(son.frame) = 0
后来,parent视图的bounds变为(0, 0, 0, 0),此时若输出son视图的frame,会发现frame = (0 50; 0 0)。也就是说这个时候
CGRectGetMinY(parent.bounds) - CGRectGetMinY(son.frame) = -50
CGRectGetMaxY(parent.bounds) - CGRectGetMaxY(son.frame) = -50
这个结果和我所理解的UIViewAutoresizingFlexibleHeight矛盾了。
矛盾就出在,如果要严格按照“不变”的规则行事的话,此时son.frame.size.height应该为-50才对。
但是尺寸怎么可以是负值呢,可能autoresizing出于这个考虑,将-50的高度处理成了0。这也就说明,在程序执行到这一步的时候,失去了-50这个信息。
最后,当parent视图的bounds回到(0, 0, 100, 100)的时候,autoresizing保持了
CGRectGetMinY(parent.bounds) - CGRectGetMinY(son.frame) = -50
CGRectGetMaxY(parent.bounds) - CGRectGetMaxY(son.frame) = -50
这个关系不变,导致了son视图的高度最终变成了100。看一眼代码最终执行的结果,果然是这样。
所以,我的bug就出在,同事因为他的业务需要,改变了我负责的最外层视图的frame,而后又将其改回原值(他以为什么也不会发生,我开始也这么以为……),这个过程中,我负责的子视图在autoresize时出现了刚才的问题,导致最后呈现出了错误的布局。
在解决这个问题的时候,我还想到了,如果autoresizing不将负值的宽高转换成0,会怎样呢?
试了试,发现UIView会自动将负值的宽高转换成正值。
比如执行完这几行代码后
UIView *view = [[UIView alloc] init];
CGRect rect = CGRectMake(100, 100, -50, -50);
view.frame = rect;
打印view视图,会发现
<UIView: 0x7ff88bd4db00; frame = (50 50; 50 50); layer = <CALayer: 0x7ff88bd4d330>>
view视图的frame被转换了。
所以不管怎么样,使用autoresizing时,如果不小心让某个视图的宽高变成了负值,则一定会损失一部分信息,导致最后即便父视图的frame回到原值,子视图的frame也会出现问题。