[译]Auto Layout的最佳实践
原文链接:https://medium.com/@NSomar/auto-layout-best-practices-for-minimum-pain-c130b2b1a0f6#.tqby1u8l4 Auto Layout是个很棒的工具,作为开发者,它可以让我们保持神志清醒,还能让我们这些懒人们在设置frame的时候远离“神奇数字”。 但是任何技术都不是完美无缺的,我必须得说我花了太多的时间来debug那些缺失的约束条件,或者对于一些藏在层级结构深处的视图,添加一个冲突的约束条件就会把整个布局毁掉,当这些事情发生的时候简直是天崩地裂! 在debug了无数个小时的auto layout的问题后,我发现每次造成问题的都是我自己(或者是你自己!),而问题的解决办法总是相同的:遵从auto layout的文档和规则! 我会在这里把正确使用auto layout的最佳实践说给你听,这样你就可以免除一些痛苦了。 每个UIView的子类都应该实现intrinsicContentSize,并且返回它认为合适的大小。 假设我们新建了一个AwesomeView,而且我们知道这个view的默认尺寸是300x20,我们会这么写:
UIView的子类应该实现intrinsicContentSize方法
-(CGSize)intrinsicContentSize { return CGSizeMake(300, 20); }
如果我们不知道view的宽度,我们会用UIViewNoIntrinsicMetric来代替:
- (CGSize)intrinsicContentSize { return CGSizeMake(UIViewNoIntrinsicMetric, 20); }
UIView基类的updateConstraints实现会调用intrinsicContentSize,它会使用返回的尺寸来给AwesomeView添加约束条件。
根据上面例子中的(300,20)尺寸,会添加下面的约束条件:
<NSContentSizeLayoutConstraint:0x7fef48d52580 H:[AwesomeView:0x7fef48ead7f0(300)] Hug:250 CompressionResistance:750>, <NSContentSizeLayoutConstraint:0x7fef48d4d110 V:[AwesomeView:0x7fef48ead7f0(20)] Hug:250 CompressionResistance:750>
添加的约束条件比较特殊,它们是NSContentSizeLayoutConstraint类型的,这个类是个私有类。这些约束条件的优先级范围是0-1000,“包住限制”(译者注:hug consistance,使其在“内容大小”的基础上不能继续变大)的优先级是250,“撑住限制”(compression resistance,撑住使其在在其“内容大小”的基础上不能继续变小)的优先级是750,使用的常量等于通过intrinsicContentSize返回的值。
请注意,UIView基类实现updateConstraints只有在它第一次执行的时候才会添加intrinsicContentSize约束。
UIView的子类绝不应该给自身的尺寸添加约束
每个view都会负责给它的superview设置约束,但是view绝不应该设置它自己的约束条件,不管是对于自身的约束(比如说 NSLayoutAttributeWidth和 NSLayoutAttributeHeight),还是相对于superview的约束。
如果一个view想指定自己的高度或者宽度,它应该通过实现 intrinsicContentSize来达到目的。
这是个糟糕的例子:
- (instancetype)init { self = [super init]; if (self) { [self addConstraint:[NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:0 multiplier:0 constant:100]]; [self addConstraint:[NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:0 multiplier:0 constant:100]]; } return self; }
这个view在通过给自身添加约束来设置自己的宽度和高度,那么如果现在它的superview也在试图指定这些数值会发生什么呢?
//Some place in the superview [awesome addConstraint:[NSLayoutConstraint constraintWithItem:awesome attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:0 multiplier:0 constant:200]]; [awesome addConstraint:[NSLayoutConstraint constraintWithItem:awesome attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:0 multiplier:0 constant:200]];
嘭!
Unable to simultaneously satisfy constraints. … property translatesAutoresizingMaskIntoConstraints) ( “<NSLayoutConstraint:0x7ff3b16c2ae0 H:[AwesomeView:0x7ff3b16bfa00(100)]>”, “<NSLayoutConstraint:0x7ff3b16c2330 H:[AwesomeView:0x7ff3b16bfa00(200)]>” ) Will attempt to recover by breaking constraint <NSLayoutConstraint:0x7ff3b16c2330 H:[AwesomeView:0x7ff3b16bfa00(200)]> …
AwesomeView添加的宽度/高度是100/100,而它的superview也添加了宽度/高度,但是是200/200,这样autoLayout就不知道该选择哪个约束条件了,因为它们的优先级都一样。
一种解决办法是这样的,将AwesomeView自身的优先级降低一点。
[self addConstraint:({ NSLayoutConstraint *constraint; constraint = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:0 multiplier:0 constant:100]; constraint.priority = 800; constraint; })];
这样autoLayout就可以做出选择了,因为它自身添加的优先级比较低,它就可以选择superview添加的约束了。
然而,尽管这样可以解决问题,但正确的方式是通过 intrinsicContentSize来指定它的高度。
UIView的子类绝不应该给它的superview添加约束
和上面的原因一样,子视图绝不应该给他的父视图添加约束。子视图的位置是由父视图决定的。
像这么做很糟糕:
- (void)didMoveToSuperview { [super didMoveToSuperview]; [self.superview addConstraint:[NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.superview attribute:NSLayoutAttributeCenterX multiplier:1 constant:0]]; }
太糟糕了,它不能离开相对于它的父视图的位置。所以,当父视图想把Awesome放到另一个位置上会怎么样?是的,就会抛出来另一个Unable to simultaneously satisfy constraints的问题。
updateConstraints是用来更新约束条件的
顾名思义,updateConstraints只是被用来更新需要的约束的。一个正确的实现 updateConstraints的方式应该长这个样子:
- (instancetype)init { … init stuff … _labelCenterYConstraints = [NSLayoutConstraint constraintWithItem:label attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]; [self addConstraint:_labelCenterYConstraints]; label.text = @”I Am truly awesome!”; … } - (void)updateConstraints { self.labelCenterYConstraints.constant = self.labelVerticalDisplacement; [super updateConstraints]; }
然后在需要的地方:
awesome.labelVerticalDisplacement = 40; [awesome setNeedsUpdateConstraints];
调用 setNeedsUpdateConstraints会使autoLayout重新计算布局,因此会调用 updateConstraints,从而读取新的label状态并更新约束。
在上面的例子中,你本可以只更新 _labelCenterYConstraints这个约束,如果你的视图暴露出约束,或者如果你可以简单获得一个约束,那就直接设置约束的常量好了,不必使用updateConstraints。所以,上面的代码也可以这么实现:
awesome.labelCenterYConstraints.constant = 40;</pre>一个非常糟糕的 updateConstraints的实现会长这个样子:
</span>- (void)updateConstraints { [self removeConstraints:self.constraints]; /* create the constraint here */ [super updateConstraints]; }
这么做是非常错误的,因为:
- 系统调用 updateConstraints很多次,因此移除或者重新创建可能会校验约束的合理性。
- [self removeConstraints:self.constraints]; 会移除包括xib或storyboard创建的所有约束条件,你该怎么重新创建这些约束?(赶紧说你不能!)
- 上面的updateConstraints实现会覆盖掉intrinsicContentSize的效果,因为你在调用[super updateConstraints];后移除了系统添加的约束条件。
- updateConstraints应该被用来创建约束条件一次,然后仅仅移除掉失效的约束。它绝不该是一个移除所有约束再把每个传过来的布局添加上的地方。(感谢Alexis提供以下补充。)
正确的实现方式是这样:
- (void)updateConstraints { if (!didSetConstraints) { didSetConstraints = YES; //create the constraint here } //Update the constraints if needed [super updateConstraints]; }
在上面的代码中,创建约束的动作只会执行一次,然后在接下来的updateConstraints调用中,只会对这些已创建的约束的常量进行修改。
我一直在追寻真理!所以如果你有很多更好的实践经验,请在推ter上和我分享。
来自:http://www.calios.gq/2015/12/14/[译]Auto-Layout的最佳实践-——-止疼片/