Ответ 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:
должен быть максимальным размером описания.
Дополнительные ресурсы
Хотя печально думать, что такое рутинное требование должно требовать "исследовательских ресурсов", я думаю, что это так. Хорошие примеры и дискуссии:
- http://www.objc.io/issue-3/advanced-auto-layout-toolbox.html
- http://devetc.org/code/2014/07/07/auto-layout-and-views-that-wrap.html
- для сеанса WWDC2014 232, "Расширенные пользовательские интерфейсы с представлениями коллекции"