UICollectionView Ячейки с автоматическими размерами
Я пытаюсь получить самоконтроль UICollectionViewCells
работая с Auto Layout, но я не могу заставить ячейки соответствовать размеру контента. У меня возникают проблемы с пониманием того, как размер ячейки обновляется из содержимого того, что находится внутри contentView ячейки.
Вот настройки, которые я пробовал:
- изготовленный на заказ
UICollectionViewCell
сUITextView
в его contentView. - Прокрутка для
UITextView
выключен. - Горизонтальное ограничение contentView: "H:|[_textView(320)]", то есть
UITextView
прикреплен слева от ячейки с явной шириной 320. - Вертикальное ограничение contentView: "V:|-0-[_textView]", то есть
UITextView
прикреплен к верхней части клетки. UITextView
имеет ограничение высоты, установленное на константу, которуюUITextView
отчеты будут соответствовать тексту.
Вот как это выглядит с красным цветом фона ячейки, а UITextView
фон установлен на синий:
Я разместил проект, с которым я играл, на GitHub здесь.
18 ответов
Обновлено для Swift 4
systemLayoutSizeFittingSize
переименован в systemLayoutSizeFitting
Обновлено для iOS 9
После того, как мой iOS GitHub перестал работать под iOS 9, у меня наконец-то появилось время полностью исследовать проблему. Сейчас я обновил репо, чтобы включить несколько примеров различных конфигураций для ячеек с самоконтролем. Мой вывод заключается в том, что самоконтролирующие клетки хороши в теории, но на практике беспорядочные Слово предостережения при работе с самоконтролирующими клетками.
TL; DR
Проверьте мой проект GitHub
Ячейки самостоятельного размера поддерживаются только в макете потока, поэтому убедитесь, что это то, что вы используете.
Есть две вещи, которые вы должны настроить для работы с саморазмерными ячейками.
1 комплект estimatedItemSize
на UICollectionViewFlowLayout
Макет потока станет динамичным по своей природе, как только вы установите estimatedItemSize
имущество.
self.flowLayout.estimatedItemSize = CGSize(width: 100, height: 100)
2. Добавить поддержку для определения размеров на вашем подклассе ячейки
Это входит в 2 аромата; Авто-макет или пользовательское переопределение preferredLayoutAttributesFittingAttributes
,
Создание и настройка ячеек с помощью Auto Layout
Я не буду вдаваться в подробности об этом, так как есть замечательный пост SO о настройке ограничений для ячейки. Просто будьте осторожны, что Xcode 6 сломал кучу вещей с iOS 7, поэтому, если вы поддерживаете iOS 7, вам нужно будет сделать что-то вроде того, чтобы убедиться, что autoresizingMask установлен в contentView ячейки, а границы contentView установлены как границы ячейки, когда клетка загружена (т.е. awakeFromNib
).
Вы должны знать, что ваша ячейка должна быть более серьезно ограничена, чем ячейка табличного представления. Например, если вы хотите, чтобы ваша ширина была динамической, тогда ваша ячейка нуждается в ограничении высоты. Аналогично, если вы хотите, чтобы высота была динамической, вам понадобится ограничение ширины для вашей ячейки.
Воплощать в жизнь preferredLayoutAttributesFittingAttributes
в вашей пользовательской ячейке
Когда эта функция вызывается, ваше представление уже настроено с контентом (т.е. cellForItem
был назван). Предполагая, что ваши ограничения были установлены соответствующим образом, у вас может быть такая реализация:
//forces the system to do one layout pass
var isHeightCalculated: Bool = false
override func preferredLayoutAttributesFittingAttributes(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
//Exhibit A - We need to cache our calculation to prevent a crash.
if !isHeightCalculated {
setNeedsLayout()
layoutIfNeeded()
let size = contentView.systemLayoutSizeFitting(layoutAttributes.size)
var newFrame = layoutAttributes.frame
newFrame.size.width = CGFloat(ceilf(Float(size.width)))
layoutAttributes.frame = newFrame
isHeightCalculated = true
}
return layoutAttributes
}
ПРИМЕЧАНИЕ. В iOS 9 поведение немного изменилось, что может привести к сбоям в вашей реализации, если вы не будете осторожны (подробнее здесь). Когда вы реализуете preferredLayoutAttributesFittingAttributes
вам нужно убедиться, что вы изменяете фрейм атрибутов макета только один раз. Если вы этого не сделаете, макет будет вызывать вашу реализацию бесконечно и в конечном итоге вылетит. Одним из решений является кэширование вычисленного размера в вашей ячейке и аннулирование этого в любое время, когда вы повторно используете ячейку или меняете ее содержимое, как я сделал с isHeightCalculated
имущество.
Испытайте свой макет
На этом этапе у вас должны быть "функционирующие" динамические ячейки в вашем collectionView. Я еще не нашел готового решения во время моих тестов, поэтому не стесняйтесь комментировать, если у вас есть. Это все еще похоже на UITableView
ИМХО побеждает в битве за динамический размер
Предостережения
Помните, что если вы используете ячейки-прототипы для вычисления timateItemSize - это сломается, если ваш XIB использует классы размеров. Причина этого заключается в том, что при загрузке ячейки из XIB ее класс размера будет настроен с Undefined
, Это будет нарушено только на iOS 8 и выше, так как на iOS 7 класс размера будет загружаться в зависимости от устройства (iPad = Обычный-Любой, iPhone = Компактный-Любой). Вы можете либо установить timateItemSize без загрузки XIB, либо вы можете загрузить ячейку из XIB, добавить ее в collectionView (это установит traitCollection), выполнить макет, а затем удалить его из суперпредставления. В качестве альтернативы вы можете также сделать вашу ячейку переопределить traitCollection
получить и вернуть соответствующие черты. Тебе решать.
Дайте мне знать, если я что-то пропустил, надеюсь, я помог и удачи в кодировании
В iOS10 появилась новая константа под названием UICollectionViewFlowLayoutAutomaticSize
так что вместо:
self.flowLayout.estimatedItemSize = CGSize(width: 100, height: 100)
Вы можете использовать это:
self.flowLayout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
Это имеет лучшую производительность, особенно когда ячейки в вашем представлении коллекции имеют постоянный вид
Несколько ключевых изменений в ответе Даниэля Галаско устранили все мои проблемы. К сожалению, у меня недостаточно репутации, чтобы комментировать напрямую (пока).
На шаге 1 при использовании автоматического макета просто добавьте один родительский UIView в ячейку. ВСЕ внутри клетки должно быть подвидом родителя. Это ответило на все мои проблемы. Хотя XCode добавляет это для UITableViewCells автоматически, этого не происходит (но должно быть) для UICollectionViewCells. Согласно документам:
Чтобы настроить внешний вид вашей ячейки, добавьте представления, необходимые для представления содержимого элемента данных в виде подпредставлений, к представлению в свойстве contentView. Не добавляйте подпредставления напрямую в саму ячейку.
Затем полностью пропустите шаг 3. Это не нужно
В iOS 10+ это очень простой двухэтапный процесс.
Убедитесь, что все содержимое вашей ячейки размещено в одном UIView (или внутри потомка UIView, такого как UIStackView, который значительно упрощает автоматическое расположение). Как и в случае динамического изменения размера UITableViewCells, для всей иерархии представления должны быть настроены ограничения, от самого внешнего контейнера до самого внутреннего представления. Это включает в себя ограничения между UICollectionViewCell и непосредственным дочерним видом
Поручите разметку вашего UICollectionView для размера автоматически
yourFlowLayout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
Добавить flowLayout для viewDidLoad()
override func viewDidLoad() { super.viewDidLoad() if let flowLayout = infoCollection.collectionViewLayout as? UICollectionViewFlowLayout { flowLayout.estimatedItemSize = CGSize(width: 1, height:1) } }
Кроме того, установите UIView в качестве mainContainer для вашей ячейки и добавьте в нее все необходимые представления.
Обратитесь к этому удивительному, умопомрачительному учебнику для дальнейшей справки: UICollectionView с авторазмером ячейки с использованием автоматического размещения в iOS 9 и 10
Поработав с этим некоторое время, я заметил, что изменение размера не работает для UITextViews, если вы не отключите прокрутку:
let textView = UITextView()
textView.scrollEnabled = false
Загадка якоря contentView:
В одном причудливом случае это
contentView.translatesAutoresizingMaskIntoConstraints = false
не будет работать. Добавил четыре явных якоря в contentView, и это сработало.
class AnnoyingCell: UICollectionViewCell {
@IBOutlet var word: UILabel!
override init(frame: CGRect) {
super.init(frame: frame); common() }
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder); common() }
private func common() {
contentView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
contentView.leftAnchor.constraint(equalTo: leftAnchor),
contentView.rightAnchor.constraint(equalTo: rightAnchor),
contentView.topAnchor.constraint(equalTo: topAnchor),
contentView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
}
и как обычно
estimatedItemSize = UICollectionViewFlowLayout.automaticSize
в YourLayout: UICollectionViewFlowLayout
Кто знает? Может кому-нибудь помочь.
Кредит
https://www.vadimbulavin.com/collection-view-cells-self-sizing/
наткнулся там на наводку - больше нигде не видел из всех статей на эту тему.
Я сделал динамическую высоту ячейки представления коллекции. Вот репозиторий git hub.
И выясните, почему предпочитаемое значение LayoutAttributesFittingAttributes вызывается более одного раза. На самом деле, он будет вызван как минимум 3 раза.
1-й предпочтительный LayoutAttributesFittingAttributes:
(lldb) po layoutAttributes
<UICollectionViewLayoutAttributes: 0x7fa405c290e0> index path: (<NSIndexPath: 0xc000000000000016>
{length = 2, path = 0 - 0}); frame = (15 12; 384 57.5);
(lldb) po self.collectionView
<UICollectionView: 0x7fa40606c800; frame = (0 57.6667; 384 0);
LayoutAttributes.frame.size.height является текущим состоянием 57.5.
2-й предпочитаемый LayoutAttributesFittingAttributes:
(lldb) po layoutAttributes
<UICollectionViewLayoutAttributes: 0x7fa405c16370> index path: (<NSIndexPath: 0xc000000000000016>
{length = 2, path = 0 - 0}); frame = (15 12; 384 534.5);
(lldb) po self.collectionView
<UICollectionView: 0x7fa40606c800; frame = (0 57.6667; 384 0);
Высота каркаса ячейки изменилась до 534,5, как и ожидалось. Но представление коллекции все еще нулевой высоты.
3-й предпочитаемый LayoutAttributesFittingAttributes:
(lldb) po layoutAttributes
<UICollectionViewLayoutAttributes: 0x7fa403d516a0> index path: (<NSIndexPath: 0xc000000000000016>
{length = 2, path = 0 - 0}); frame = (15 12; 384 534.5);
(lldb) po self.collectionView
<UICollectionView: 0x7fa40606c800; frame = (0 57.6667; 384 477);
Вы можете видеть, что высота просмотра коллекции была изменена с 0 на 477.
Поведение похоже на ручку прокрутки:
1. Before self-sizing cell
2. Validated self-sizing cell again after other cells recalculated.
3. Did changed self-sizing cell
Сначала я думал, что этот метод вызывается только один раз. Поэтому я закодировал следующее:
CGRect frame = layoutAttributes.frame;
frame.size.height = frame.size.height + self.collectionView.contentSize.height;
UICollectionViewLayoutAttributes* newAttributes = [layoutAttributes copy];
newAttributes.frame = frame;
return newAttributes;
Эта строка:
frame.size.height = frame.size.height + self.collectionView.contentSize.height;
вызовет бесконечный цикл системного вызова и сбой приложения.
При любом изменении размера, он будет снова и снова проверять все ячейки selectedLayoutAttributesFittingAttributes до тех пор, пока позиции каждой ячейки (т. Е. Кадры) больше не изменятся.
Решение состоит из 3 простых шагов:
- Включение динамического изменения размера ячеек
flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
- Установите containerView.widthAnchor.constraint из
collectionView(:cellForItemAt:)
чтобы ограничить ширину contentView шириной collectionView.
class ViewController: UIViewController, UICollectionViewDataSource {
...
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath) as! MultiLineCell
cell.textView.text = dummyTextMessages[indexPath.row]
cell.maxWidth = collectionView.frame.width
return cell
}
...
}
class MultiLineCell: UICollectionViewCell{
....
var maxWidth: CGFloat? {
didSet {
guard let maxWidth = maxWidth else {
return
}
containerViewWidthAnchor.constant = maxWidth
containerViewWidthAnchor.isActive = true
}
}
....
}
Поскольку вы хотите включить автоматическое изменение размера UITextView, у него есть дополнительный шаг:
3. Вычислите и установите heightAnchor.constant для UITextView.
Итак, всякий раз, когда устанавливается ширина contentView, мы настраиваем высоту UITextView в didSet
из maxWidth
.
Внутри UICollectionViewCell:
var maxWidth: CGFloat? {
didSet {
guard let maxWidth = maxWidth else {
return
}
containerViewWidthAnchor.constant = maxWidth
containerViewWidthAnchor.isActive = true
let sizeToFitIn = CGSize(width: maxWidth, height: CGFloat(MAXFLOAT))
let newSize = self.textView.sizeThatFits(sizeToFitIn)
self.textViewHeightContraint.constant = newSize.height
}
}
Эти шаги принесут вам желаемый результат.
Полная работоспособность
Ссылка: сообщение в блоге Вадима Булавина - Самостоятельное определение размеров ячеек представления коллекции: пошаговое руководство
Скриншот:
В дополнение к ответам выше,
Просто удостоверьтесь, что вы установили для свойства оценочного свойства UIollectionViewFlowLayout некоторый размер и не реализуете метод делегата sizeForItem:atIndexPath.
Вот и все.
Приведенный выше пример метода не компилируется. Вот исправленная версия (но не проверенная относительно того, работает ли она.)
override func preferredLayoutAttributesFittingAttributes(layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes
{
let attr: UICollectionViewLayoutAttributes = layoutAttributes.copy() as! UICollectionViewLayoutAttributes
var newFrame = attr.frame
self.frame = newFrame
self.setNeedsLayout()
self.layoutIfNeeded()
let desiredHeight: CGFloat = self.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
newFrame.size.height = desiredHeight
attr.frame = newFrame
return attr
}
Для всех, кто попробовал все без удачи, это единственное, что заставило меня работать. Для многострочных меток внутри ячейки попробуйте добавить эту волшебную строку:
label.preferredMaxLayoutWidth = 200
Больше информации: здесь
Ура!
Кому бы это ни помогло,
У меня был этот неприятный сбой, если estimatedItemSize
был установлен. Даже если я вернул 0 в numberOfItemsInSection
, Следовательно, сами ячейки и их автоматическое расположение не были причиной сбоя... CollectionView просто сбой, даже если он пуст, просто потому, что estimatedItemSize
был установлен для самостоятельного определения размеров.
В моем случае я реорганизовал свой проект, из контроллера, содержащего collectionView, в collectionViewController, и он работал.
Пойди разберись.
Если вы реализуете метод UICollectionViewDelegateFlowLayout:
- (CGSize)collectionView:(UICollectionView*)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath*)indexPath
Когда вы звоните collectionview performBatchUpdates:completion:
, размер высота будет использовать sizeForItemAtIndexPath
вместо preferredLayoutAttributesFittingAttributes
,
Процесс рендеринга performBatchUpdates:completion
пройдем через метод preferredLayoutAttributesFittingAttributes
но он игнорирует ваши изменения.
В моем классе просмотра коллекции есть: ☝️
override var contentSize: CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
Вид моей коллекции: ☝️ (Для автоматического изменения размера ячеек)
let buttonsCollectionView: ContentCollectionView = {
var cv = ContentCollectionView()
if let collectionViewLayout = cv.collectionViewLayout as? UICollectionViewFlowLayout {
collectionViewLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
}
return cv
}()
buttonsCollectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0),
buttonsCollectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0),
Ячейка выглядит так:
NSLayoutConstraint.activate([
title.topAnchor.constraint(equalTo: topAnchor, constant: 16),
title.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
title.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
title.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -16),
])
Обновите больше информации:
Если вы используете
flowLayout.estimatedItemSize
Предлагаю использовать iOS8.3 более поздней версии. До iOS8.3 он вылетит[super layoutAttributesForElementsInRect:rect];
, Сообщение об ошибке*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil'
Во-вторых, в версии iOS8.x,
flowLayout.estimatedItemSize
приведет к тому, что настройка вставки в другом разделе не работает. т.е. функция:(UIEdgeInsets)collectionView:layout:insetForSectionAtIndex:
,
В
Swift 5
, меня устраивает.
UICollectionViewFlowLayout:
estimatedItemSize = UICollectionViewFlowLayout.automaticSize
UICollectionViewCell:
(пс: я использую SnapKit)
class Cell: UICollectionViewCell {
let customizedContentView = UIView()
...
func layoutAction() {
contentView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(customizedContentView)
customizedContentView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
}
тогда вам просто нужно расширить
customizedContentView
.
Я пытался с помощью estimatedItemSize
но было множество ошибок при вставке и удалении ячеек, если estimatedItemSize
не был точно равен высоте клетки. я перестал устанавливать estimatedItemSize
и реализовал динамические ячейки, используя прототип ячейки. вот как это делается:
создать этот протокол:
protocol SizeableCollectionViewCell {
func fittedSize(forConstrainedSize size: CGSize)->CGSize
}
реализовать этот протокол в вашем обычае UICollectionViewCell
:
class YourCustomCollectionViewCell: UICollectionViewCell, SizeableCollectionViewCell {
@IBOutlet private var mTitle: UILabel!
@IBOutlet private var mDescription: UILabel!
@IBOutlet private var mContentView: UIView!
@IBOutlet private var mTitleTopConstraint: NSLayoutConstraint!
@IBOutlet private var mDesciptionBottomConstraint: NSLayoutConstraint!
func fittedSize(forConstrainedSize size: CGSize)->CGSize {
let fittedSize: CGSize!
//if height is greatest value, then it's dynamic, so it must be calculated
if size.height == CGFLoat.greatestFiniteMagnitude {
var height: CGFloat = 0
/*now here's where you want to add all the heights up of your views.
apple provides a method called sizeThatFits(size:), but it's not
implemented by default; except for some concrete subclasses such
as UILabel, UIButton, etc. search to see if the classes you use implement
it. here's how it would be used:
*/
height += mTitle.sizeThatFits(size).height
height += mDescription.sizeThatFits(size).height
height += mCustomView.sizeThatFits(size).height //you'll have to implement this in your custom view
//anything that takes up height in the cell has to be included, including top/bottom margin constraints
height += mTitleTopConstraint.constant
height += mDescriptionBottomConstraint.constant
fittedSize = CGSize(width: size.width, height: height)
}
//else width is greatest value, if not, you did something wrong
else {
//do the same thing that's done for height but with width, remember to include leading/trailing margins in calculations
}
return fittedSize
}
}
Теперь сделайте ваш контроллер в соответствии с UICollectionViewDelegateFlowLayout
и в нем есть это поле:
class YourViewController: UIViewController, UICollectionViewDelegateFlowLayout {
private var mCustomCellPrototype = UINib(nibName: <name of the nib file for your custom collectionviewcell>, bundle: nil).instantiate(withOwner: nil, options: nil).first as! SizeableCollectionViewCell
}
он будет использоваться в качестве ячейки-прототипа для привязки данных, а затем для определения того, как эти данные повлияли на измерение, которое вы хотите динамически
наконец, UICollectionViewDelegateFlowLayout's
collectionView(:layout:sizeForItemAt:)
должно быть реализовано:
class YourViewController: UIViewController, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {
private var mDataSource: [CustomModel]
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath)->CGSize {
//bind the prototype cell with the data that corresponds to this index path
mCustomCellPrototype.bind(model: mDataSource[indexPath.row]) //this is the same method you would use to reconfigure the cells that you dequeue in collectionView(:cellForItemAt:). i'm calling it bind
//define the dimension you want constrained
let width = UIScreen.main.bounds.size.width - 20 //the width you want your cells to be
let height = CGFloat.greatestFiniteMagnitude //height has the greatest finite magnitude, so in this code, that means it will be dynamic
let constrainedSize = CGSize(width: width, height: height)
//determine the size the cell will be given this data and return it
return mCustomCellPrototype.fittedSize(forConstrainedSize: constrainedSize)
}
}
и это все. Возвращая размер ячейки в collectionView(:layout:sizeForItemAt:)
таким образом, мешая мне использовать estimatedItemSize
и вставка и удаление ячеек работает отлично.