是否应将UICollectionViewCell子类的contentView.translatesAutoResizingMaskToConstraints设置为“false”?

时间:2016-07-04 23:26:04

标签: ios autolayout uicollectionview uicollectionviewcell

TL; DR

当尝试通过自动布局调整UICollectionViewCells的大小时,即使是一个简单的示例,您也可以轻松获得自动布局警告。

我们应该设置contentView.translatesAutoResizingMaskToConstraints = false来摆脱它们吗?

我尝试使用self sizing auto layout cells创建一个UICollectionView。

在viewDidLoad中:

let layout = UICollectionViewFlowLayout()
layout.estimatedItemSize = CGSize(width: 10, height: 10)
collectionView.collectionViewLayout = layout

我的细胞非常基本。它是一个宽度和高度为75的蓝色视图,用于测试目的。通过将视图固定到所有4个边上的超视图,并为其赋予高度和宽度来创建约束。

class MyCell: UICollectionViewCell {
  override init(frame: CGRect) {
    view = UIView()

    super.init(frame: frame)

    view.backgroundColor = UIColor.blueColor()
    contentView.addSubview(view)

    installConstraints()
  }

  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  var view: UIView

  func installConstraints() {
    view.translatesAutoresizingMaskIntoConstraints = false

    var c: NSLayoutConstraint

    // pin all edges
    c = NSLayoutConstraint(item: contentView, attribute: .Leading, relatedBy: .Equal, toItem: view, attribute: .Leading, multiplier: 1, constant: 0)
    c.active = true
    c = NSLayoutConstraint(item: contentView, attribute: .Trailing, relatedBy: .Equal, toItem: view, attribute: .Trailing, multiplier: 1, constant: 0)
    c.active = true
    c = NSLayoutConstraint(item: contentView, attribute: .Top, relatedBy: .Equal, toItem: view, attribute: .Top, multiplier: 1, constant: 0)
    c.active = true
    c = NSLayoutConstraint(item: contentView, attribute: .Bottom, relatedBy: .Equal, toItem: view, attribute: .Bottom, multiplier: 1, constant: 0)
    c.active = true

    // set width and height
    c = NSLayoutConstraint(item: view, attribute: .Width, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: 75)
    c.active = true
    c = NSLayoutConstraint(item: view, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: 75)
    c.active = true
  }
}

运行代码时,我得到两个关于无法同时满足约束的错误:

Unable to simultaneously satisfy constraints.
    Probably at least one of the constraints in the following list is one you don't want. 
    Try this: 
        (1) look at each constraint and try to figure out which you don't expect; 
        (2) find the code that added the unwanted constraint or constraints and fix it. 
    (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints) 
(
    "<NSAutoresizingMaskLayoutConstraint:0x7fed88c1bac0 h=--& v=--& H:[UIView:0x7fed8a90c2f0(10)]>",
    "<NSLayoutConstraint:0x7fed8ab770b0 H:[UIView:0x7fed8a90bbe0(75)]>",
    "<NSLayoutConstraint:0x7fed8a90d610 UIView:0x7fed8a90c2f0.leading == UIView:0x7fed8a90bbe0.leading>",
    "<NSLayoutConstraint:0x7fed8ab005f0 H:[UIView:0x7fed8a90bbe0]-(0)-|   (Names: '|':UIView:0x7fed8a90c2f0 )>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x7fed8ab770b0 H:[UIView:0x7fed8a90bbe0(75)]>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.

(省略了关于垂直约束的第二个错误,但它几乎相同。)

这些错误是有道理的。实质上,contentView(0x7fed8a90c2f0)具有translatesAutoresizingMask = true,因此其边界设置为10x10(来自估计的大小)会创建NSAutoresizingMaskLayoutConstraint约束。视图无法同时显示10像素宽和75像素宽,因此会输出错误。

我尝试了几种不同的方法来解决这个问题,其中只有2个似乎有效。

解决方案1:将至少一个约束设置为999或更小

在这种情况下,如果我更改将蓝色视图底部固定到contentView底部的约束为999优先级(并对尾随约束执行相同操作),则可以解决问题。同样,我可以选择Top + Leading,或Width + Height,只要我不在每个方向都有1000个优先级。至少有一个人必须&#34;给予&#34;对于初始布局。

虽然听起来可能会导致视图的高度/宽度不正确,但实际上看起来大小正确。

解决方案2:设置contentView.translatesAutoresizingMaskIntoConstraints = false

通过将其设置为false,它基本上摆脱了NSAutoresizingMaskLayoutConstraint约束,我们不再有冲突。另一方面,由于contentView没有设置为自动布局,所以无论你如何改变它的框架,它的超级视图(单元格)永远不会更新它的大小,因为它有什么都没有&#34;推动&#34;反对它。

然而,这个解决方案在某种程度上神奇地起作用。我假设在某个时刻,单元格会查看contentView的框架并决定&#34;嘿,你把我设置为这个大小,这可能意味着你希望单元格也是这样大小的#34;并负责为您调整单元格大小。

我们应该使用哪种解决方案?

这个问题有更好的解决方案吗?应该使用以上哪个?上面提到的两种解决方案是否存在任何缺点,或者它们基本相同,并且您使用哪种解决方案并不重要?

注意:

  • DGSelfSizingCollectionViewCells等示例中难以看到的原因是因为它们依赖于intrinsicContentSize + contentCompressionResistancePriority。如果你摆脱了我的宽度+高度限制,用1000 setContentCompressionResistancePriority调用(水平+垂直)替换它们,并使用具有内在大小的东西(例如带有文本的标签),你将不再看到自动布局错误。这意味着width@1000与内在宽度+ contentCompressionResistance@1000的行为不同。也许视图获得其固有宽度的时间是可以修改内容视图的框架。

那些没有用的东西:

  • 我注意到当我通过故事板创建单元格时,我通常不会遇到这个问题。当我检查通过故事板创建的单元格的view和以编程方式创建的单元格的差异时,我注意到故事板中的autoresizingMask是RM + BM(36),但是通过代码创建时为0 。我尝试手动将我的蓝色视图的autoresizingMask设置为36,但这并不起作用。故事板解决方案的真正原因在于,您创建的约束通常已在故事板中完美设置(否则您将需要修复故事板错误)。作为修复这些错误的一部分,您可以更改单元格的边界。由于它调用init?(coder:),因此已正确配置了单元格的边界。所以即使是contentView.autoresizingMask = true,也没有冲突,因为它已经以正确的大小定义了。
  • 在链接指南中,当谈到创建约束时,它链接到this post。它提到应该在updateConstraints中创建约束。然而,听起来这是Apple的原始建议,并且对于大多数约束,它们都是no longer recommending这个约定。然而,由于该文章说要在updateConstraints中创建它们,我试图无济于事。我得到了相同的结果。这是因为在调用updateConstraints时它没有更新帧(它仍然是10x10)。
  • 当您选择的估计尺寸太小时,我发现了类似的问题。我通过增加估计的大小来修复它。在这种情况下,我尝试了100x100,但它仍然导致相同的错误。这是有道理的......一个视图不能同时是100宽和75宽。 (请注意,我考虑将其设置为75x75非解决方案。有关详细信息,请参阅下文。)

非解决方案:

  • 将估计尺寸设置为75x75。虽然这将解决这个简单示例的错误,但它不会解决真正动态调整大小的单元格的问题。我想要一个能够无处不在的解决方案。
  • collectionView(_:layout:sizeForItemAtIndexPath:)中返回单元格的确切尺寸(例如,通过创建尺寸单元格)。我想要一个直接适用于我的手机的解决方案,无需任何记账或额外的计算。

1 个答案:

答案 0 :(得分:3)

在测试完毕后,我发现至少有一个理由要保持contentView.translatesAutoresizingMaskToConstraints = true

如果您使用preferredLayoutAttributesFittingAttributes(_:)来更改UICollectionViewCell的尺寸,则可以通过将contentView的大小调整为正确的尺寸来实现。如果您设置contentView.translatesAutoresizingMaskToConstraints = false,则会丢失此功能。

因此,我建议使用解决方案1(在每个维度中至少更改一个约束是非必需的)。事实上,我为UICollectionViewCell创建了一个包装器,它将处理所需的999约束,以及一种使首选高度或宽度正常工作的方法。

通过使用此包装器,您无需记住使UICollectionViewCell中的contentView正常运行的复杂性。

class CollectionViewCell<T where T: UIView>: UICollectionViewCell {

  override init(frame: CGRect) {
    preferredHeight = nil
    preferredWidth = nil

    super.init(frame: frame)
  }

  var preferredWidth: CGFloat?
  var preferredHeight: CGFloat?
  private(set) var view: T?

  func initializeView(view: T) {
    assert(self.view == nil)
    self.view = view

    contentView.addSubview(view)

    view.translatesAutoresizingMaskIntoConstraints = false

    var constraint: NSLayoutConstraint

    constraint = NSLayoutConstraint(item: view, attribute: .Top, relatedBy: .Equal, toItem: contentView, attribute: .Top, multiplier: 1, constant: 0)
    constraint.active = true
    constraint = NSLayoutConstraint(item: view, attribute: .Leading, relatedBy: .Equal, toItem: contentView, attribute: .Leading, multiplier: 1, constant: 0)
    constraint.active = true

    // Priority must be less than 1000 to prevent errors when installing
    // constraints in conjunction with the contentView's autoresizing constraints.
    let NonRequiredPriority: UILayoutPriority = UILayoutPriorityRequired - 1

    constraint = NSLayoutConstraint(item: view, attribute: .Bottom, relatedBy: .Equal, toItem: contentView, attribute: .Bottom, multiplier: 1, constant: 0)
    constraint.priority = NonRequiredPriority
    constraint.active = true
    constraint = NSLayoutConstraint(item: view, attribute: .Trailing, relatedBy: .Equal, toItem: contentView, attribute: .Trailing, multiplier: 1, constant: 0)
    constraint.priority = NonRequiredPriority
    constraint.active = true
  }

  override func preferredLayoutAttributesFittingAttributes(layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    let newLayoutAttributes = super.preferredLayoutAttributesFittingAttributes(layoutAttributes)

    if let preferredHeight = preferredHeight {
      newLayoutAttributes.bounds.size.height = preferredHeight
    }
    if let preferredWidth = preferredWidth {
      newLayoutAttributes.bounds.size.width = preferredWidth
    }

    return newLayoutAttributes
  }
}

(注意:由于bug with generic subclasses of UICollectionViewCell,因此需要使用init方法。)

注册:

collectionView.registerClass(CollectionViewCell<UIView>.self, forCellWithReuseIdentifier: "Cell")

使用:

func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
  let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! CollectionViewCell<UILabel>

  if cell.view == nil {
    cell.initializeView(UILabel())
  }

  cell.view!.text = "Content"
  cell.preferredHeight = collectionView.bounds.height

  return cell
}