UICollectionView самостоятельной размеров клеток с авто макет


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

вот настройка, которую я пробовал:

  • Custom UICollectionViewCell С UITextView в его contentView.
  • скролл для UITextView отключено.
  • на странице просмотр содержимого это горизонтальное ограничение: "H: / [_textView (320)]", т. е. UITextView закрепляется слева от ячейки с явной шириной 320.
  • на странице просмотр содержимого является: "V:/-0-[_textView]", т. е. UITextView прикреплен к верхней части ячейки.
  • The UITextView имеет ограничение по высоте, установленное на константу, которая UITextView отчеты будут соответствовать тексту.

вот как это выглядит с фоном ячейки, установленным в красный цвет, и UITextView фон установить на синий:

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

13 154

13 ответов:

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

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


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

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

TL; DR

проверьте мои проект GitHub


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

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

1. Набор estimatedItemSize on UICollectionViewFlowLayout

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

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

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 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 выигрывает битву за динамическую калибровку ИМХО.

предостережения

будьте очень внимательны, что если вы используете прототип ячейки для расчета estimatedItemSize - это сломается, если ваш XIB использует классы размере. Причина этого в том, что когда вы загружаете свою ячейку из XIB его размер класса будет настроен с Undefined. Это будет нарушено только на iOS 8 и выше, так как на iOS 7 класс размера будет загружен на основе устройства (iPad = обычный-любой, iPhone = компактный-любой). Вы можете либо установить estimatedItemSize без загрузки XIB, либо вы можете загрузить ячейку из XIB, добавить ее в collectionView (это установит traitCollection), выполнить макет, а затем удалить его из супервизора. В качестве альтернативы вы также можете сделать свою ячейку переопределить traitCollection getter и возвращает соответствующие черты. Это зависит от тебя.

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


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

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

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

затем полностью пропустите шаг 3. В этом нет необходимости.

В iOS10 есть новая константа под названием UICollectionViewFlowLayoutAutomaticSize, Так что вместо этого:

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

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

self.flowLayout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize

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

в iOS 10+ это очень простой 2 этапа.

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

  2. проинструктируйте flowlayout вашего 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 с ячейкой авторазмера с помощью autolayout в iOS 9 & 10

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

let textView = UITextView()
textView.scrollEnabled = false

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

и, выкопать, почему preferredLayoutAttributesFittingattributes вызывается несколько раз. На самом деле, он будет называться не менее 3 раз.

изображение журнала консоли : enter image description here

1-й 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);

layoutAttributes.рамка.размер.высота-это текущее состояние 57.5.

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

3-й 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;

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

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

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

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

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

Go фигура.

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

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
}

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

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

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

процесс визуализации performBatchUpdates:completion пройдет через метод preferredLayoutAttributesFittingAttributes но он игнорирует ваши изменения.

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

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

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

label.preferredMaxLayoutWidth = 200

Подробнее: здесь

Ура!

Я пробовал использовать 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, и вставка и удаление ячеек отлично работает.