Должно ли contentView.translatesAutoResizingMaskToConstraints подкласса UICollectionViewCell установить значение `false`?

TL; DR

При попытке настроить UICollectionViewCells с помощью автоматического макета вы можете легко получить предупреждения о макетах с помощью простого примера.

Должны ли мы установить contentView.translatesAutoResizingMaskToConstraints = false, чтобы избавиться от них?


Я пытаюсь создать 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 приоритетов в каждом направлении. По крайней мере, один должен "дать" для первоначальной компоновки.

Хотя может показаться, что это приведет к тому, что представление будет иметь неправильную высоту/ширину, на самом деле это будет правильный размер.

Решение 2: Установите contentView.translatesAutoresizingMaskIntoConstraints = false

Установив это на false, он по существу избавляется от ограничения NSAutoresizingMaskLayoutConstraint, и у нас больше нет конфликта. С другой стороны, поскольку ContentView не настроен с автоматическим макетом для начала, он как бы независимо от того, как вы меняете свой фрейм, его супервизор (ячейка) никогда не будет обновлять свой размер, потому что ничего не "толкает", назад против него.

Тем не менее, это решение как-то волшебным образом работает. Я предполагаю, что в какой-то момент ячейка просматривает кадр contentView и решает: "Эй, вы установили меня в этот размер, это, вероятно, означает, что вы хотите, чтобы ячейка тоже была такого размера" и позаботится о том, чтобы изменить размер ячейки для вас.

Какое решение мы должны использовать?

Есть ли лучшие решения этой проблемы? Какое из указанных выше должно быть использовано? Существуют ли какие-либо недостатки для двух упомянутых выше решений или они по существу одинаковы, и не имеет значения, какой из них вы используете?


Примечания:

  • Причина, которую труднее увидеть в примерах, таких как DGSelfSizingCollectionViewCells, заключается в том, что они полагаются на intrinsicContentSize + contentCompressionResistancePriority. Если вы избавитесь от ограничений ширины и высоты, замените их на 1000 вызовов setContentCompressionResistancePriority (горизонтально + вертикально) и используйте что-то с внутренним размером (например, с меткой с текстом), вы больше не увидите ошибки автоматического макета. Это означает, что [email protected] ведет себя по-разному, чем intrinsicWidth + [email protected]. Возможно, время, когда представление получает свою внутреннюю ширину, после того, как это нормально, изменит кадр представления содержимого.

Вещи, которые не сработали:

  • Я заметил, что когда я создаю ячейки через раскадровку, я обычно никогда не сталкиваюсь с этой проблемой. Когда я исследовал различия в view ячейки, созданной с помощью раскадровки, и один был создан программно, я заметил, что autoresizingMask был RM + BM (36) в раскадровке, но 0 при создании через код. Я попытался вручную установить autoresizingMask моего синего представления на 36, но это не сработало. Настоящая причина, по которой работает раскадровка, заключается в том, что обычно создаваемые вами ограничения уже отлично установлены в раскадровке (в противном случае у вас были бы ошибки раскадровки, которые необходимо исправить). В рамках исправления этих ошибок вы можете изменить границы ячейки. Поскольку он вызывает init?(coder:), границы ячейки уже настроены правильно. Таким образом, несмотря на то, что contentView.autoresizingMask = true нет конфликтов, поскольку он уже определен в правильном размере.
  • В связанном руководстве он ссылается на этот посткогда речь идет о создании ограничений. В нем упоминается, что ограничения должны быть созданы в updateConstraints. Однако, похоже, это были оригинальные рекомендации Apple и что они больше не рекомендуют это соглашение для большинства ваших ограничений. Тем не менее, поскольку в этой статье написано, чтобы создать их в updateConstraints, я попытался безрезультатно. Я получил те же результаты. Это связано с тем, что кадр не обновлялся по времени, когда был вызван updateConstraints (он все еще был 10x10).
  • Я заметил подобные проблемы, когда вы выбрали слишком маленький расчетный размер. Я исправил это в прошлом, увеличив оценочный размер. В этом случае я попробовал 100x100, но он по-прежнему вызывал те же ошибки. Это имеет смысл... вид не может быть одновременно шириной в 100 и 75 шир. (Обратите внимание, что я считаю, что установить его на 75x75 не-решение. См. Ниже для получения дополнительной информации.)

Non-решения:

  • Настройка расчетного размера до 75x75. Хотя это приведет к устранению ошибок для этого простого примера, это не решит проблему для ячеек, которые действительно имеют динамический размер. Я хочу решение, которое будет работать везде.
  • Возврат точных размеров ячейки в collectionView(_:layout:sizeForItemAtIndexPath:) (например, путем создания ячейки калибровки). Я хочу, чтобы решение, которое работает с моей ячейкой напрямую, не требует каких-либо бухгалтерских или дополнительных расчетов.

Ответ 1

После тестирования, я заметил хотя бы одну причину, чтобы сохранить contentView.translatesAutoresizingMaskToConstraints = true.

Если вы используете preferredLayoutAttributesFittingAttributes(_:) для изменения размеров UICollectionViewCell, он работает путем определения размера contentView в правильном размере. Если вы установите contentView.translatesAutoresizingMaskToConstraints = false, вы потеряете эту функциональность.

Поэтому я рекомендую использовать Решение 1 (изменение хотя бы одного ограничения в каждом измерении необязательно). Фактически, я создал оболочку для UICollectionViewCell, которая будет обрабатывать как требуемое ограничение 999, так и способ получить правильную высоту или ширину для правильной работы.

Используя эту оболочку, вам не нужно будет помнить о тонкостях, с помощью которых contentView в UICollectionViewCell ведет себя правильно.


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 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
  }
}

(Примечание: метод init требуется из-за ошибки с общими подклассами UICollectionViewCell.)

Для регистрации:

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
}