[译]Auto Layout的最佳实践

jopen 8年前

原文链接: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方法

每个UIView的子类都应该实现intrinsicContentSize,并且返回它认为合适的大小。

假设我们新建了一个AwesomeView,而且我们知道这个view的默认尺寸是300x20,我们会这么写:

-(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的最佳实践-——-止疼片/