Подтвердить что ты не робот

UICollectionView Self Sizing Cells с автоматической компоновкой

Я пытаюсь UICollectionViewCells для самостоятельного определения UICollectionViewCells работая с автоматическим UICollectionViewCells, но я не могу заставить ячейки соответствовать размеру содержимого. У меня возникают проблемы с пониманием того, как обновляется размер ячейки, исходя из содержимого содержимого ячейки contentView.

Вот настройки, которые я пробовал:

  • Пользовательский UICollectionViewCell с UITextView в его contentView.
  • Прокрутка для UITextView отключена.
  • Горизонтальное ограничение contentView: "H: | [_textView (320)]", то есть UITextView закрепляется слева от ячейки с явной шириной 320.
  • Вертикальное ограничение contentView: "V: | -0 - [_ textView]", то есть UITextView закрепленный в верхней части ячейки.
  • UITextView имеет ограничение высоты, установленное на константу, которую отчеты UITextView поместят в текст.

Вот как это выглядит, если фон ячейки установлен в красный цвет, а фон UITextView установлен в синий цвет: cell background red, UITextView background blue

Я разместил проект, с которым я играл, на GitHub здесь.

4b9b3361

Ответ 1

Обновлено для Swift 5

preferredLayoutAttributesFittingAttributes переименован в preferredLayoutAttributesFitting и используется авторазмер


Обновлено для Swift 4

systemLayoutSizeFittingSize переименован в systemLayoutSizeFitting


Обновлено для iOS 9

После того, как мой iOS GitHub перестал работать под iOS 9, у меня наконец-то появилось время полностью исследовать проблему. Сейчас я обновил репо, чтобы включить несколько примеров различных конфигураций для ячеек с самоконтролем. Мой вывод состоит в том, что самоконтролирующие клетки хороши в теории, но на практике беспорядочные. Слово предостережения при работе с самоконтролирующими клетками.

TL; DR

Проверьте мой проект GitHub


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

Есть две вещи, которые вы должны настроить для работы с самоконтролем ячеек.

1. Установите estimatedItemSize на UICollectionViewFlowLayout

Макет потока станет динамичным по своей природе после того, как вы установите свойство estimatedItemSize.

self.flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize

2. Добавить поддержку для определения размеров в вашем подклассе ячейки

Это входит в 2 аромата; Автоматическая разметка или пользовательского переопределения preferredLayoutAttributesFittingAttributes.

Создание и настройка ячеек с помощью автоматического макета

Я не буду вдаваться в подробности об этом, поскольку есть блестящая ТАК публикация о настройке ограничений для ячейки. Просто будьте осторожны, что 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 preferredLayoutAttributesFitting(_ 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 и возвращать соответствующие черты. Это до вас.

Дайте мне знать, если я что-то пропустил, надеюсь, я помог и удачи в кодировании


Ответ 2

Несколько ключевых изменений в ответе Даниэля Галаско устранили все мои проблемы. К сожалению, у меня недостаточно репутации, чтобы комментировать напрямую (пока).

На шаге 1 при использовании автоматического макета просто добавьте один родительский UIView в ячейку. ВСЕ внутри клетки должно быть подвидом родителя. Это ответило на все мои проблемы. Хотя XCode добавляет это для UITableViewCells автоматически, этого не происходит (но должно быть) для UICollectionViewCells. Согласно документам:

Чтобы настроить внешний вид вашей ячейки, добавьте представления, необходимые для представления содержимого элементов данных в виде подпредставлений, к представлению в свойстве contentView. Не добавляйте подпредставления напрямую в саму ячейку.

Затем полностью пропустите шаг 3. Это не нужно

Ответ 3

В iOS10 есть новая константа с именем UICollectionViewFlowLayout.automaticSize (ранее UICollectionViewFlowLayoutAutomaticSize), поэтому вместо этого:

self.flowLayout.estimatedItemSize = CGSize(width: 100, height: 100)

Вы можете использовать это:

self.flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize

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

Доступ к макету потока:

override func viewDidLoad() {
   super.viewDidLoad()

   if let flowLayout = collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
      flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
   }
}

Ответ 4

В iOS 10+ это очень простой двухэтапный процесс.

  1. Убедитесь, что все содержимое вашей ячейки размещено в одном UIView (или внутри потомка UIView, такого как UIStackView, который значительно упрощает автоматическое расположение). Как и в случае динамического изменения размера UITableViewCells, для всей иерархии представления должны быть настроены ограничения, от самого внешнего контейнера до самого внутреннего представления. Это включает в себя ограничения между UICollectionViewCell и непосредственным дочерним видом

  2. Поручите разметку вашего UICollectionView, чтобы размер автоматически

    yourFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
    

Ответ 5

  1. Добавить flowLayout для viewDidLoad()

    override func viewDidLoad() {
        super.viewDidLoad() 
        if let flowLayout = infoCollection.collectionViewLayout as? UICollectionViewFlowLayout {
            flowLayout.estimatedItemSize = CGSize(width: 1, height:1)
        }
    }
    
  2. Кроме того, установите UIView в качестве mainContainer для вашей ячейки и добавьте в нее все необходимые представления.

  3. Обратитесь к этому удивительному, умопомрачительному уроку для дальнейшей ссылки: UICollectionView с авторазмером ячейки с использованием автоматического размещения в iOS 9 и 10

Ответ 6

После некоторого времени борьбы с этим я заметил, что изменение размера не работает для UITextViews, если вы не отключите прокрутку:

let textView = UITextView()
textView.scrollEnabled = false

Ответ 7

Я сделал динамическую высоту ячейки коллекции. Вот git hub repo.

И, выкапывайте, почему preferredLayoutAttributesFittingAttributes вызывается более одного раза. Фактически, это будет называться как минимум 3 раза.

Изображение журнала консоли: введите описание изображения здесь

1st preferredLayoutAttributesFittingAttributes:

(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);

МакетAttributes.frame.size.height - текущий статус 57.5.

2nd preferredLayoutAttributesFittingAttributes:

(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, как и ожидалось. Но, просмотр коллекции все еще имеет нулевую высоту.

3rd preferredLayoutAttributesFittingAttributes:

(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;

вызовет бесконечный цикл системного вызова и сбой приложения.

Любой размер изменен, он будет проверять все предпочтительные атрибутыLayoutAttributesFittingAttributes каждой ячейки, пока все позиции ячеек (например, кадры) больше не будут изменены.

Ответ 8

Если вы реализуете метод UICollectionViewDelegateFlowLayout:

- (CGSize)collectionView:(UICollectionView*)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath*)indexPath

Когда вы вызываете collectionview performBatchUpdates:completion:, высота размера будет использовать sizeForItemAtIndexPath вместо preferredLayoutAttributesFittingAttributes.

Процесс рендеринга performBatchUpdates:completion будет проходить через метод preferredLayoutAttributesFittingAttributes, но он игнорирует ваши изменения.

Ответ 9

Кому бы это ни помогло,

У меня был этот неприятный сбой, если был установлен estimatedItemSize. Даже если я вернул 0 в numberOfItemsInSection. Поэтому сами ячейки и их автоматическая компоновка не были причиной сбоя... CollectionView просто разбился, даже когда пуст, только потому, что estimatedItemSize был установлен для саморазмера.

В моем случае я реорганизовал свой проект с контроллера, содержащего collectionView, в collectionViewController и работал.

Показать рисунок.

Ответ 10

Приведенный выше примерный метод не компилируется. Вот исправленная версия (но не проверена, работает она или нет).

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
}

Ответ 11

Обновить дополнительную информацию:

  • Если вы используете 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:.

Ответ 12

Для тех, кто пробовал все без везения, это единственное, что заставило его работать на меня. Для многострочных меток внутри ячейки попробуйте добавить эту магическую строку:

label.preferredMaxLayoutWidth = 200

Дополнительная информация: здесь

Ура!

Ответ 13

Я объединил ответы fooobar.com/questions/8498/... и fooobar.com/questions/8498/... и реализовал следующее решение, которое действительно хорошо работает с autolayout (просто не забудьте установить все необходимые ограничения): fooobar.com/questions/17254048/...

Я решил поместить один UIView со всеми необходимыми дочерними представлениями внутри него в UICollecitonViewCell

Я установил UIView тянущийся, ведущий, верхний к ICollecitonViewCell но я не установил дно к нижнему виду представления ячейки (вы должны использовать последнее дочернее представление внутри UIView и соединять их основания, а не с представлением ячейки)

Затем я добавил ссылку на UIView в свой пользовательский класс ячеек и использую его высоту для высоты ячейки:

public class MyCustomItemCell: UICollectionViewCell {

    // other child views references ...

    @IBOutlet weak var wrapperView: UIView! // view which contains your other views

    //forces the system to do one layout pass
    var isHeightCalculated: Bool = false

    override public func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
        //Exhibit A - We need to cache our calculation to prevent a crash.
        if !isHeightCalculated {
            setNeedsLayout()
            layoutIfNeeded()
            var newFrame = layoutAttributes.frame
            newFrame.size.height = wrapperView.frame.height // use height of our UIView
            layoutAttributes.frame = newFrame
            isHeightCalculated = true
        }
        return layoutAttributes
    }
}

Ответ 14

В дополнение к ответам выше,

Просто удостоверьтесь, что вы установили для свойства оценочного свойства UIollectionViewFlowLayout некоторый размер и не реализуете метод делегата sizeForItem: atIndexPath.

Это.

Ответ 15

тайна привязки 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/

наткнулся на верхушку там - никогда не видел его больше нигде во всех тысячах статей об этом.

Ответ 16

Я попытался использовать 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 dynamic, so it must be calculated
        if size.height == CGFLoat.greatestFiniteMagnitude {

            var height: CGFloat = 0

            /*now here where you want to add all the heights up of your views.
              apple provides a method called sizeThatFits(size:), but it 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 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 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, а вставка и удаление ячеек работает отлично.