Указание одного измерения ячеек в UICollectionView с использованием автоматической компоновки

В iOS 8 UICollectionViewFlowLayout поддерживает автоматическое изменение размеров ячеек на основе их собственного размера содержимого. Это изменяет размеры ячеек как по ширине, так и по высоте в зависимости от их содержимого.

Можно ли указать фиксированное значение для ширины (или высоты) всех ячеек и изменить размеры других размеров?

Для простого примера рассмотрим многострочную метку в ячейке с ограничениями, позиционирующими ее по сторонам ячейки. Многострочная метка может быть изменена различными способами для размещения текста. Ячейка должна заполнить ширину представления коллекции и соответствующим образом отрегулировать ее высоту. Вместо этого ячейки имеют случайный размер и даже приводят к сбою, когда размер ячейки больше, чем размер прокручиваемого вида коллекции.


В iOS 8 вводится метод systemLayoutSizeFittingSize: withHorizontalFittingPriority: verticalFittingPriority:. Для каждой ячейки в представлении коллекции макет вызывает этот метод в ячейке, передавая примерный размер. Мне было бы лучше переопределить этот метод на ячейке, передать размер, который задан, и установить горизонтальное ограничение и низкий приоритет для вертикального ограничения. Таким образом, горизонтальный размер фиксируется на значение, установленное в макете, и размер по вертикали может быть гибким.

Что-то вроде этого:

- (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes {
    UICollectionViewLayoutAttributes *attributes = [super preferredLayoutAttributesFittingAttributes:layoutAttributes];
    attributes.size = [self systemLayoutSizeFittingSize:layoutAttributes.size withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityFittingSizeLevel];
    return attributes;
}

Однако размеры, приведенные в этом методе, совершенно странны. Документация по этому методу очень неясна для меня и упоминает использование констант UILayoutFittingCompressedSize UILayoutFittingExpandedSize, которые просто представляют нулевой размер и довольно большой.

Является ли параметр size этого метода просто способом передачи двух констант? Невозможно ли достичь такого поведения, которое я ожидаю от получения соответствующей высоты для заданного размера?


Альтернативные решения

1) Добавление ограничений, которые будут указывать конкретную ширину ячейки, обеспечивает правильную компоновку. Это плохое решение, потому что это ограничение должно быть установлено на размер представления коллекции ячеек, к которому у него нет безопасной ссылки. Значение для этого ограничения может быть передано, когда ячейка настроена, но это также кажется совершенно противоречивым. Это также неудобно, потому что добавление ограничений непосредственно в ячейку или просмотр содержимого вызывает много проблем.

2) Используйте представление таблицы. Представления таблиц работают таким образом из коробки, поскольку ячейки имеют фиксированную ширину, но это не будет учитывать другие ситуации, такие как макет iPad с ячейками фиксированной ширины в нескольких столбцах.

Ответ 1

Похоже на то, что вы просите, это способ использовать UICollectionView для создания макета, такого как UITableView. Если это действительно то, что вы хотите, правильный способ сделать это с помощью пользовательского подкласса UICollectionViewLayout (возможно, что-то вроде SBTableLayout).

С другой стороны, если вы действительно спрашиваете, есть ли чистый способ сделать это с помощью UICollectionViewFlowLayout по умолчанию, то я считаю, что нет никакого способа. Даже с iOS8 самонастраивающимися ячейками это не просто. Основная проблема, как вы говорите, заключается в том, что механизм компоновки потоков не дает возможности исправить одно измерение и позволить другому реагировать. (Кроме того, даже если бы вы могли, возникла бы дополнительная сложность вокруг необходимости двух макетов, чтобы размер многострочных меток. Это может не соответствовать тому, как ячейки для самостоятельной калибровки хотят вычислить все размеры с помощью одного вызова systemLayoutSizeFittingSize.)

Однако, если вы все же хотите создать макет таблицы, подобный таблице, с макетом потока, с ячейками, которые определяют их собственный размер, и естественно реагировать на ширину просмотра коллекции, конечно, это возможно. Все еще беспорядочно. Я сделал это с помощью "ячейки калибровки", то есть не отображаемого UICollectionViewCell, который контроллер сохраняет только для расчета размеров ячеек.

В этом подходе есть две части. Первая часть предназначена для делегата представления коллекции, чтобы вычислить правильный размер ячейки, взяв ширину представления коллекции и используя ячейку калибровки для вычисления высоты ячейки.

В вашем UICollectionViewDelegateFlowLayout вы реализуете такой метод:

func collectionView(collectionView: UICollectionView,
  layout collectionViewLayout: UICollectionViewLayout,
  sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
{
  // NOTE: here is where we say we want cells to use the width of the collection view
   let requiredWidth = collectionView.bounds.size.width

   // NOTE: here is where we ask our sizing cell to compute what height it needs
  let targetSize = CGSize(width: requiredWidth, height: 0)
  /// NOTE: populate the sizing cell contents so it can compute accurately
  self.sizingCell.label.text = items[indexPath.row]
  let adequateSize = self.sizingCell.preferredLayoutSizeFittingSize(targetSize)
  return adequateSize
}

Это приведет к тому, что представление коллекции установит ширину ячейки на основе содержимого коллекции, но затем попросите ячейку калибровки вычислить высоту.

Вторая часть - заставить ячейку калибровки использовать свои собственные ограничения AL для вычисления высоты. Это может быть сложнее, чем должно быть, из-за того, что многолинейный UILabel эффективно требует двухэтапного процесса компоновки. Работа выполняется в методе preferredLayoutSizeFittingSize, который выглядит так:

 /*
 Computes the size the cell will need to be to fit within targetSize.

 targetSize should be used to pass in a width.

 the returned size will have the same width, and the height which is
 calculated by Auto Layout so that the contents of the cell (i.e., text in the label)
 can fit within that width.

 */
 func preferredLayoutSizeFittingSize(targetSize:CGSize) -> CGSize {

   // save original frame and preferredMaxLayoutWidth
   let originalFrame = self.frame
   let originalPreferredMaxLayoutWidth = self.label.preferredMaxLayoutWidth

   // assert: targetSize.width has the required width of the cell

   // step1: set the cell.frame to use that width
   var frame = self.frame
   frame.size = targetSize
   self.frame = frame

   // step2: layout the cell
   self.setNeedsLayout()
   self.layoutIfNeeded()
   self.label.preferredMaxLayoutWidth = self.label.bounds.size.width

   // assert: the label bounds and preferredMaxLayoutWidth are set to the width required by the cell width

   // step3: compute how tall the cell needs to be

   // this causes the cell to compute the height it needs, which it does by asking the 
   // label what height it needs to wrap within its current bounds (which we just set).
   let computedSize = self.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)

   // assert: computedSize has the needed height for the cell

   // Apple: "Only consider the height for cells, because the contentView isn't anchored correctly sometimes."
   let newSize = CGSize(width:targetSize.width,height:computedSize.height)

   // restore old frame and preferredMaxLayoutWidth
   self.frame = originalFrame
   self.label.preferredMaxLayoutWidth = originalPreferredMaxLayoutWidth

   return newSize
 }

(Этот код адаптирован из примера кода Apple из примера кода сессии WWDC2014 в разделе "Расширенный просмотр коллекции".)

Пара замечает. Он использует layoutIfNeeded() для принудительной компоновки всей ячейки, чтобы вычислить и установить ширину метки. Но этого недостаточно. Я считаю, что вам также нужно установить preferredMaxLayoutWidth, чтобы метка использовала эту ширину с автоматическим макетом. И только тогда вы можете использовать systemLayoutSizeFittingSize, чтобы заставить ячейку вычислить ее высоту, принимая во внимание метку.

Мне нравится этот подход? Нет!! Он чувствует себя слишком сложным, и он делает макет дважды. Но до тех пор, пока производительность не станет проблемой, я предпочел бы выполнить макет дважды во время выполнения, чем определять его дважды в коде, что, по-видимому, является единственной альтернативой.

Моя надежда состоит в том, что в конечном итоге ячейки для самостоятельной калибровки будут работать по-другому, и все это будет намного проще.

Пример проекта, отображающий его на рабочем месте.

Но почему бы просто не использовать ячейки для самостоятельной калибровки?

В теории, новые возможности iOS8 для "самоустанавливающихся ячеек" должны сделать это ненужным. Если вы определили ячейку с автоматической компоновкой (AL), то представление коллекции должно быть достаточно умным, чтобы позволить самому размеру и корректно выстроить. На практике я не видел примеров, которые заставили это работать с многострочными метками. Я думаю, что это отчасти из-за того, что механизм ячеек для самостоятельной калибровки по-прежнему неисправен.

Но я бы поспорил, в основном из-за обычной хитрости Auto Layout и ярлыков, а именно, что UILabels требует в основном двухэтапный макет. Мне непонятно, как вы можете выполнять оба шага с ячейками самостоятельной калибровки.

И, как я уже сказал, это действительно работа для другого макета. Он является частью сущности макета потока, что он позиционирует вещи, размер которых, а не фиксирует ширину и позволяет им выбирать их высоту.

А как насчет preferredLayoutAttributesFittingAttributes:?

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

И что с systemlayoutSizeFittingSize:?

Вы правы, документы сбивают с толку.

Документы на systemLayoutSizeFittingSize: и systemLayoutSizeFittingSize:withHorizontalFittingPriority:verticalFittingPriority: показывают, что вы должны передавать только UILayoutFittingCompressedSize и UILayoutFittingExpandedSize в качестве targetSize. Однако сама подпись метода, комментарии заголовка и поведение функций указывают на то, что они отвечают на точное значение параметра targetSize.

Фактически, если вы установите UICollectionViewFlowLayoutDelegate.estimatedItemSize, чтобы включить новый механизм ячеек для самостоятельной калибровки, это значение, как представляется, передается как targetSize. И UILabel.systemLayoutSizeFittingSize, похоже, возвращает те же самые значения, что и UILabel.sizeThatFits. Это подозрительно, учитывая, что аргумент systemLayoutSizeFittingSize должен быть грубой целью, а аргумент sizeThatFits: должен быть максимальным размером описания.

Дополнительные ресурсы

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

Ответ 2

Там более чистый способ сделать это, чем некоторые другие ответы здесь, и он работает хорошо. Он должен быть реалистичным (просмотр коллекции загружается быстро, без лишних автоматических макетов и т.д.), И не имеет никаких "магических чисел", таких как ширина фиксированного вида коллекции. Изменение размера вида коллекции, например. при вращении, а затем недействительность макета также должна работать отлично.

1. Создайте подкласс подкласса

class HorizontallyFlushCollectionViewFlowLayout: UICollectionViewFlowLayout {

    // Don't forget to use this class in your storyboard (or code, .xib etc)

    override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
        let attributes = super.layoutAttributesForItemAtIndexPath(indexPath)?.copy() as? UICollectionViewLayoutAttributes
        guard let collectionView = collectionView else { return attributes }
        attributes?.bounds.size.width = collectionView.bounds.width - sectionInset.left - sectionInset.right
        return attributes
    }

    override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let allAttributes = super.layoutAttributesForElementsInRect(rect)
        return allAttributes?.flatMap { attributes in
            switch attributes.representedElementCategory {
            case .Cell: return layoutAttributesForItemAtIndexPath(attributes.indexPath)
            default: return attributes
            }
        }
    }
}

2. Зарегистрируйте просмотр коллекции для автоматической калибровки

// The provided size should be a plausible estimate of the actual
// size. You can set your item size in your storyboard
// to a good estimate and use the code below. Otherwise,
// you can provide it manually too, e.g. CGSize(width: 100, height: 100)

flowLayout.estimatedItemSize = flowLayout.itemSize

3. Используйте предопределенную ширину + пользовательскую высоту в подклассе ячейки

override func preferredLayoutAttributesFittingAttributes(layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    layoutAttributes.bounds.size.height = systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
    return layoutAttributes
}

Ответ 3

Простой способ сделать это в iOS 9 в нескольких строках кодов - пример горизонтали (фиксация его высоты до высоты его коллекции):

Инициируйте макет Flow Flow Collection с помощью estimatedItemSize, чтобы включить ячейку для самостоятельной калибровки:

self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.estimatedItemSize = CGSizeMake(1, 1);

Реализовать делегат макета представления коллекций (чаще всего в вашем контроллере просмотра), collectionView:layout:sizeForItemAtIndexPath:. Цель здесь - установить фиксированную высоту (или ширину) в размер коллекции. Значение 10 может быть любым, но вы должны установить его на значение, которое не нарушает ограничений:

- (CGSize)collectionView:(UICollectionView *)collectionView
                  layout:(UICollectionViewLayout *)collectionViewLayout
  sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
    return CGSizeMake(10, CGRectGetHeight(collectionView.bounds));
}

Переопределите свой собственный метод preferredLayoutAttributesFittingAttributes:, эта часть фактически рассчитает вашу динамическую ширину ячеек на основе ваших ограничений автоматической компоновки и только что установленной высоты:

- (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes
{
    UICollectionViewLayoutAttributes *attributes = [layoutAttributes copy];
    float desiredWidth = [self.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].width;
    CGRect frame = attributes.frame;
    frame.size.width = desiredWidth;
    attributes.frame = frame;
    return attributes;
}

Ответ 4

Попробуйте установить ширину в предпочтительных атрибутах раскладки:

- (UICollectionViewLayoutAttributes *)preferredLayoutAttributesFittingAttributes:(UICollectionViewLayoutAttributes *)layoutAttributes {
    UICollectionViewLayoutAttributes *attributes = [[super preferredLayoutAttributesFittingAttributes:layoutAttributes] copy];
    CGSize newSize = [self systemLayoutSizeFittingSize:CGSizeMake(FIXED_WIDTH,layoutAttributes.size) withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityFittingSizeLevel];
    CGRect newFrame = attr.frame;
    newFrame.size.height = size.height;
    attr.frame = newFrame;
    return attr;
}

Естественно, вы также хотите убедиться, что вы правильно настроили свой макет:

UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout *) self.collectionView.collectionViewLayout;
flowLayout.estimatedItemSize = CGSizeMake(FIXED_WIDTH, estimatedHeight)];

Вот что я положил на Github, который использует ячейки с постоянной шириной и поддерживает динамический тип, чтобы высота ячеек обновлялась как системный шрифт изменения размера.

Ответ 5

ДА это можно сделать с помощью автоматической компоновки программно и путем установки ограничений в раскадровке или xib. Вам нужно добавить ограничение для размера ширины, чтобы оно оставалось постоянным и устанавливало высоту, большую или равную.
http://www.thinkandbuild.it/learn-to-love-auto-layout-programmatically/

http://www.cocoanetics.com/2013/08/variable-sized-items-in-uicollectionview/
Надеюсь, это будет полезно и решит вашу проблему.