UICollectionView Ячейки с автоматическими размерами

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

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

  • изготовленный на заказ UICollectionViewCell с UITextView в его contentView.
  • Прокрутка для UITextView выключен.
  • Горизонтальное ограничение contentView: "H:|[_textView(320)]", то есть UITextView прикреплен слева от ячейки с явной шириной 320.
  • Вертикальное ограничение contentView: "V:|-0-[_textView]", то есть UITextView прикреплен к верхней части клетки.
  • 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+ это очень простой двухэтапный процесс.

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

  2. Поручите разметку вашего UICollectionView для размера автоматически

    yourFlowLayout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
    
  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

Поработав с этим некоторое время, я заметил, что изменение размера не работает для 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 простых шагов:

  1. Включение динамического изменения размера ячеек

flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize

  1. Установите 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'scollectionView(: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и вставка и удаление ячеек работает отлично.

Другие вопросы по тегам