UICollectionView внутри UITableViewCell - динамическая высота?

125

Один из экранов нашего приложения требует, чтобы мы поместили UICollectionViewвнутри файла UITableViewCell. Это UICollectionViewбудет иметь динамическое количество элементов, в результате чего высота также должна вычисляться динамически. Однако я сталкиваюсь с проблемами при попытке рассчитать высоту встроенного UICollectionView.

Наша общая структура UIViewControllerбыла создана в раскадровках и действительно использует автоматический макет. Но я не знаю, как динамически увеличивать высоту в UITableViewCellзависимости от высоты UICollectionView.

Кто-нибудь может дать несколько советов или советов о том, как этого добиться?

Shadowman
источник
не могли бы вы рассказать немного больше о макете, который вы пытаетесь выполнить? Высота изображения UITableViewCellотличается от его фактического вида на столе? Нужна ли вертикальная прокрутка как на, так UICollectionViewи наUITableView
apouche
3
Высота UITableViewCell должна быть динамической, в зависимости от высоты UICollectionView, который является частью ячейки табличного представления. Макет моего UICollectionView - это 4 столбца и динамическое количество строк. Итак, со 100 элементами у меня будет 4 столбца и 25 строк в моем UICollectionView. Высота конкретной ячейки - как указано в UITableView heightForRowAtIndexPath - должна иметь возможность настраиваться в зависимости от размера этого UICollectionView. Это немного яснее?
Shadowman
@Shadowman, пожалуйста, предложите мне решение этой ситуации
Рави Оджа

Ответы:

127

Правильный ответ - ДА , МОЖНО это сделать.

Я столкнулся с этой проблемой несколько недель назад. На самом деле это проще, чем вы думаете. Поместите свои ячейки в NIB (или раскадровки) и закрепите их, чтобы автоматическая компоновка выполняла всю работу

Учитывая следующую структуру:

TableView

TableViewCell

CollectionView

CollectionViewCell

CollectionViewCell

CollectionViewCell

[... переменное количество ячеек или разные размеры ячеек]

Решение состоит в том, чтобы указать автоматическому макету сначала вычислить размеры collectionViewCell, затем - contentSize представления коллекции и использовать его как размер вашей ячейки. Это метод UIView, который «творит чудеса»:

-(void)systemLayoutSizeFittingSize:(CGSize)targetSize 
     withHorizontalFittingPriority:(UILayoutPriority)horizontalFittingPriority 
           verticalFittingPriority:(UILayoutPriority)verticalFittingPriority

Вы должны установить здесь размер TableViewCell, который в вашем случае является contentSize CollectionView.

CollectionViewCell

В CollectionViewCell вы должны указывать ячейке макет каждый раз, когда вы меняете модель (например: вы устанавливаете UILabel с текстом, тогда ячейка снова должна быть макетом).

- (void)bindWithModel:(id)model {
    // Do whatever you may need to bind with your data and 
    // tell the collection view cell's contentView to resize
    [self.contentView setNeedsLayout];
}
// Other stuff here...

TableViewCell

TableViewCell делает магию. Она имеет выход к вашему CollectionView, включает автоматическое расположение для ячеек CollectionView с использованием estimatedItemSize из UICollectionViewFlowLayout .

Затем уловка состоит в том, чтобы установить размер ячейки tableView в systemLayoutSizeFittingSize методе ... (ПРИМЕЧАНИЕ: iOS8 или новее)

ПРИМЕЧАНИЕ. Я попытался использовать метод высоты ячейки делегата для tableView. -(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPathНо для системы автоматической компоновки уже слишком поздно вычислять размер CollectionView contentSize, и иногда вы можете найти ячейки с неправильным размером.

@implementation TableCell

- (void)awakeFromNib {
    [super awakeFromNib];
    UICollectionViewFlowLayout *flow = (UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout;

    // Configure the collectionView
    flow.minimumInteritemSpacing = ...;

    // This enables the magic of auto layout. 
    // Setting estimatedItemSize different to CGSizeZero 
    // on flow Layout enables auto layout for collectionView cells.
    // https://developer.apple.com/videos/play/wwdc2014-226/
    flow.estimatedItemSize = CGSizeMake(1, 1);

    // Disable the scroll on your collection view
    // to avoid running into multiple scroll issues.
    [self.collectionView setScrollEnabled:NO];
}

- (void)bindWithModel:(id)model {
    // Do your stuff here to configure the tableViewCell
    // Tell the cell to redraw its contentView        
    [self.contentView layoutIfNeeded];
}

// THIS IS THE MOST IMPORTANT METHOD
//
// This method tells the auto layout
// You cannot calculate the collectionView content size in any other place, 
// because you run into race condition issues.
// NOTE: Works for iOS 8 or later
- (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize withHorizontalFittingPriority:(UILayoutPriority)horizontalFittingPriority verticalFittingPriority:(UILayoutPriority)verticalFittingPriority {

    // With autolayout enabled on collection view's cells we need to force a collection view relayout with the shown size (width)
    self.collectionView.frame = CGRectMake(0, 0, targetSize.width, MAXFLOAT);
    [self.collectionView layoutIfNeeded];

    // If the cell's size has to be exactly the content 
    // Size of the collection View, just return the
    // collectionViewLayout's collectionViewContentSize.

    return [self.collectionView.collectionViewLayout collectionViewContentSize];
}

// Other stuff here...

@end

TableViewController

Не забудьте включить систему автоматического макета для ячеек tableView в вашем TableViewController :

- (void)viewDidLoad {
    [super viewDidLoad];

    // Enable automatic row auto layout calculations        
    self.tableView.rowHeight = UITableViewAutomaticDimension;
    // Set the estimatedRowHeight to a non-0 value to enable auto layout.
    self.tableView.estimatedRowHeight = 10;

}

КРЕДИТ: @rbarbera помог разобраться в этом

Пабло Ромеу
источник
4
Да. Это работает. Это должен быть принятый ответ.
csotiriou
1
Может ли кто-нибудь предоставить мне образец кода для этого? Я хочу показать несколько изображений в вертикальном представлении коллекции, которое находится внутри ячейки табличного представления ..
Рави Оджха
4
Наконец, это сработало для меня, но мне пришлось сделать другой трюк: cell.bounds = CGRectMake(0, 0, CGRectGetWidth(tableView.bounds), 10000);это будет «имитировать» длинную ячейку с длинным collectionView, поэтому collectionView вычислит размер всех своих ячеек. В противном случае collectionView вычислял только видимые размеры ячеек и неверно устанавливал высоту talbeViewCell.
ingaham
20
при установке максимальной высоты кадра systemLayoutSizeFittingSizeя быстро столкнулся с проблемой при перезагрузке ячейки, приложения, кажется, зависают после self.collectionView.layoutIfNeeded(). Я исправил это, установив высоту рамки на 1, а не на максимальную высоту. Надеюсь, это поможет, если кто-нибудь наткнется на эту проблему.
titan
6
Вот что исправило это для меня - в systemLayoutSizeFittingобратном порядке того, что показано в ответе в настоящее время, так что сначала collectionView.layoutIfNeeded()запускается, а затемcollectionView.frame = CGRect.init(origin: .zero, size: CGSize.init(width: targetSize.width, height: 1))
xaphod
101

Я думаю, что мое решение намного проще, чем предложенное @PabloRomeu.

Шаг 1. Создайте розетку из UICollectionViewв UITableViewCell subclass, где она UICollectionViewнаходится. Пусть это будет имяcollectionView

Шаг 2. Добавьте IB для UICollectionViewограничения высоты и также создайте выход UITableViewCell subclass. Пусть, имя будет collectionViewHeight.

Шаг 3. tableView:cellForRowAtIndexPath:Добавьте код:

// deque a cell
cell.frame = tableView.bounds;
[cell layoutIfNeeded];
[cell.collectionView reloadData];
cell.collectionViewHeight.constant = cell.collectionView.collectionViewLayout.collectionViewContentSize.height;
Игорь
источник
1
Вы действительно пробовали это? Кажется невозможным одновременно установить ограничение высоты для представления коллекции и закрепить его на полях ячейки представления таблицы. У вас либо слишком мало ограничений (отсюда и предупреждение о нулевом размере), либо конфликтующие ограничения (одно из которых игнорируется). Пожалуйста, объясните, как установить ограничения для этого решения.
Dale
2
У меня все заработало. Я добавляю ограничения для collectionViewвсех сторон UITableViewCellи устанавливаю tableView.estimatedRowHeight = 44;иtableView.rowHeight = UITableViewAutomaticDimension;
Игорь
3
Этот работает как чемпион ... просто и эффективно ... спасибо. Если у кого-то это не работает, возможно, причина в том, что нам нужно привязать нижнюю часть представления коллекции к нижней части ячейки представления таблицы. Тогда он заработает. Удачного кодирования .. :)
Sebin Roy
2
Будет ли это работать при изменении размера экрана, например, при повороте iPad? Это потенциально может изменить высоту представления коллекции и, следовательно, высоту ячейки, но если на экране ячейка не обязательно может быть перезагружена представлением таблицы, поэтому метод cellForRowAtIndexPath: никогда не будет вызван и вы не сможете изменить константу ограничения. Тем не менее, возможно, удастся принудительно перезагрузить данные.
Карл Линдберг,
2
Вы пробовали разные размеры ячеек collectionView? Это была моя проблема. Если у вас разные размеры ячеек collectionView, это не сработало ... :(
Пабло Роме
22

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

Я рекомендую вам использовать только представление коллекции для всех ваших целей.

Вы можете разделить его на разделы и «рассматривать» макет некоторых разделов как вид таблицы, а других как вид коллекции. В конце концов, в представлении коллекции нет ничего, чего нельзя добиться в представлении таблицы.

Если у вас есть базовая сетка для «частей» представления коллекции, вы также можете использовать обычные ячейки таблицы для их обработки. Тем не менее, если вам не нужна поддержка iOS 5, вам лучше использовать представления коллекций.

Rivera
источник
1
Вы заключили «угощение» в кавычки, потому что НЕ можете определять разные макеты для каждого раздела CollectionView, верно? Я не нашел возможности определять разные макеты для разных разделов представления коллекции. Я что-то упускаю из виду или вы говорите о том, чтобы по-разному реагировать на layoutAttributesForItemAtIndexPath для разных разделов в моем собственном подклассе UICollectionViewLayout?
Алекс да Франка
Да -> Вы говорите о разных ответах layoutAttributesForItemAtIndexPathна разные разделы в моем собственном подклассе UICollectionViewLayout?
Ривера
2
@Rivera, ваш ответ наиболее разумный и звучит просто правильно. Я читал это предложение во многих местах. Не могли бы вы также поделиться ссылкой на образец или советом о том, как это сделать, или хотя бы некоторыми указателями. TIA
thesummersign
3
Ваш ответ предполагает, что вы начинаете с нуля и у вас еще нет большого сложного табличного представления, в которое вам нужно вписаться. В частности, был задан вопрос о размещении представления коллекции в представлении таблицы. ИМО, ваш ответ не является «ответом»
xaphod
2
Это плохой ответ. Представления коллекций внутри представлений таблиц - обычное дело, почти каждое приложение делает это.
crashoverride777 08
10

Ответ Пабло Ромеу выше ( https://stackoverflow.com/a/33364092/2704206 ) очень помог мне с моей проблемой. Однако мне пришлось сделать несколько вещей по-другому, чтобы это сработало для моей проблемы. Во-первых, мне пришлось не layoutIfNeeded()так часто звонить . Мне нужно было только вызвать его collectionViewв systemLayoutSizeFittingфункции.

Во-вторых, у меня были ограничения автоматической компоновки в моем представлении коллекции в ячейке табличного представления, чтобы дать ему некоторый отступ. Поэтому мне пришлось вычесть начальное и конечное поля из targetSize.widthпри установке collectionView.frameширины. Мне также пришлось добавить верхнее и нижнее поля к CGSizeвысоте возвращаемого значения .

Чтобы получить эти константы ограничений, у меня была возможность либо создать выходы для ограничений, жестко закодировать их константы, либо найти их по идентификатору. Я решил выбрать третий вариант, чтобы мой класс ячеек пользовательского табличного представления можно было легко повторно использовать. В конце концов, это было все, что мне нужно, чтобы все заработало:

class CollectionTableViewCell: UITableViewCell {

    // MARK: -
    // MARK: Properties
    @IBOutlet weak var collectionView: UICollectionView! {
        didSet {
            collectionViewLayout?.estimatedItemSize = CGSize(width: 1, height: 1)
            selectionStyle = .none
        }
    }

    var collectionViewLayout: UICollectionViewFlowLayout? {
        return collectionView.collectionViewLayout as? UICollectionViewFlowLayout
    }

    // MARK: -
    // MARK: UIView functions
    override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
        collectionView.layoutIfNeeded()

        let topConstraintConstant = contentView.constraint(byIdentifier: "topAnchor")?.constant ?? 0
        let bottomConstraintConstant = contentView.constraint(byIdentifier: "bottomAnchor")?.constant ?? 0
        let trailingConstraintConstant = contentView.constraint(byIdentifier: "trailingAnchor")?.constant ?? 0
        let leadingConstraintConstant = contentView.constraint(byIdentifier: "leadingAnchor")?.constant ?? 0

        collectionView.frame = CGRect(x: 0, y: 0, width: targetSize.width - trailingConstraintConstant - leadingConstraintConstant, height: 1)

        let size = collectionView.collectionViewLayout.collectionViewContentSize
        let newSize = CGSize(width: size.width, height: size.height + topConstraintConstant + bottomConstraintConstant)
        return newSize
    }
}

В качестве вспомогательной функции для получения ограничения по идентификатору я добавляю следующее расширение:

extension UIView {
    func constraint(byIdentifier identifier: String) -> NSLayoutConstraint? {
        return constraints.first(where: { $0.identifier == identifier })
    }
}

ПРИМЕЧАНИЕ. Вам нужно будет установить идентификатор для этих ограничений в раскадровке или там, где они создаются. Если у них нет константы 0, это не имеет значения. Кроме того, как и в ответе Пабло, вам нужно будет использовать UICollectionViewFlowLayoutв качестве макета для представления вашей коллекции. Наконец, убедитесь, что вы связали collectionViewIBOutlet со своей раскадровкой.

С пользовательской ячейкой табличных выше, теперь я могу подкласс в любой другом виде таблицы ячейке , которая нуждается в представлении коллекции и он реализовать UICollectionViewDelegateFlowLayoutи UICollectionViewDataSourceпротоколы. Надеюсь, это поможет кому-то другому!

jmad8
источник
5

Прочитал все ответы. Кажется, это подходит для всех случаев.

override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
        collectionView.layoutIfNeeded()
        collectionView.frame = CGRect(x: 0, y: 0, width: targetSize.width , height: 1)
        return collectionView.collectionViewLayout.collectionViewContentSize
}
SRP-Achiever
источник
3

Альтернативой решению Пабло Ромеу является настройка самого UICollectionView, а не выполнение работы в ячейке табличного представления.

Основная проблема заключается в том, что по умолчанию представление коллекции не имеет внутреннего размера и поэтому не может информировать автоматический макет об используемых измерениях. Вы можете исправить это, создав собственный подкласс, который действительно возвращает полезный внутренний размер.

Создайте подкласс UICollectionView и переопределите следующие методы

override func intrinsicContentSize() -> CGSize {
    self.layoutIfNeeded()

    var size = super.contentSize
    if size.width == 0 || size.height == 0 {
        // return a default size
        size = CGSize(width: 600, height:44)
    }

    return size
 }

override func reloadData() {
    super.reloadData()
    self.layoutIfNeeded()
    self.invalidateIntrinsicContentSize()
}

(Вы также должны переопределить связанные методы: reloadSections, reloadItemsAtIndexPaths аналогично reloadData ())

Вызов layoutIfNeeded заставляет представление коллекции пересчитать размер содержимого, который затем можно использовать в качестве нового внутреннего размера.

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

    override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator)
{
    super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
    dispatch_async(dispatch_get_main_queue()) {
        self.tableView.reloadData()
    }
}
Дол
источник
братан, не могли бы вы помочь с моей проблемой stackoverflow.com/questions/44650058/… ?
May Phyu
3

Недавно я столкнулся с той же проблемой, и я почти пробовал каждое решение в ответах, некоторые из них работали, а другие не беспокоились о подходе @PabloRomeu в том, что если у вас есть другое содержимое в ячейке (кроме представления коллекции) вам нужно будет вычислить их высоту и высоту их ограничений и вернуть результат, чтобы получить правильную автоматическую компоновку, а я не люблю вычислять вещи вручную в моем коде. Итак, вот решение, которое отлично сработало для меня, не выполняя никаких ручных вычислений в моем коде.

в виде cellForRow:atIndexPathтаблицы я делаю следующее:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    //do dequeue stuff
    //initialize the the collection view data source with the data
    cell.frame = CGRect.zero
    cell.layoutIfNeeded()
    return cell
}

Я думаю, что здесь происходит то, что я заставляю ячейку tableview регулировать свою высоту после вычисления высоты представления коллекции. (после предоставления источнику данных даты collectionView)

Абд аль-Рхман Тахер Бадари
источник
2

Я бы поместил статический метод в класс представления коллекции, который будет возвращать размер в зависимости от содержимого, которое он будет иметь. Затем используйте этот метод в, heightForRowAtIndexPathчтобы вернуть правильный размер.

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

InkGolem
источник
Что касается меня, я пытался встроить контроллер табличного представления в контроллеры представления коллекции. Каждый раз, когда ячейка используется повторно, она запоминала некоторые ограничения автоматического размещения, которые я применил к ним. Это была очень неудобная и в конечном итоге ужасная идея. Думаю, вместо этого я буду использовать только одно представление коллекции.
fatuhoku
2

Может быть, мой вариант будет полезен; Решал эту задачу последние два часа. Я не претендую на 100% правильность или оптимальность, но мои навыки еще очень малы, и я хотел бы услышать комментарии от экспертов. Спасибо. Одно важное замечание: это работает для статической таблицы - это указано в моей текущей работе. Итак, все, что я использую, это viewWillLayoutSubviews для tableView . И еще немного.

private var iconsCellHeight: CGFloat = 500 

func updateTable(table: UITableView, withDuration duration: NSTimeInterval) {
    UIView.animateWithDuration(duration, animations: { () -> Void in
        table.beginUpdates()
        table.endUpdates()
    })
}

override func viewWillLayoutSubviews() {
    if let iconsCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 1)) as? CategoryCardIconsCell {
        let collectionViewContentHeight = iconsCell.iconsCollectionView.contentSize.height
        if collectionViewContentHeight + 17 != iconsCellHeight {
            iconsCellHeight = collectionViewContentHeight + 17
            updateTable(tableView, withDuration: 0.2)
        }
    }
}

override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    switch (indexPath.section, indexPath.row) {
        case ...
        case (1,0):
            return iconsCellHeight
        default:
            return tableView.rowHeight
    }
}
  1. Я знаю, что collectionView находится в первой строке второго раздела;
  2. Пусть высота ряда 17 п. больше, чем высота его содержимого;
  3. iconsCellHeight - это случайное число при запуске программы (я знаю, что в портретной форме это должно быть ровно 392, но это не важно). Если содержимое collectionView + 17 не равно этому числу, измените его значение. В следующий раз в этой ситуации условие дает ЛОЖЬ;
  4. После всего обновите tableView. В моем случае это комбинация двух операций (для приятного обновления расширяющихся строк);
  5. И, конечно же, в heightForRowAtIndexPath добавьте одну строку в код.
Илья А. Шувалов
источник
2

Самый простой подход, который я придумал до сих пор, Кредиты на ответ @igor выше,

В вашем классе tableviewcell просто вставьте это

override func layoutSubviews() {
    self.collectionViewOutlet.constant = self.postPoll.collectionViewLayout.collectionViewContentSize.height
}

и, конечно же, замените collectionviewoutlet своим выходом в классе ячейки

Osa
источник
1
Высота камеры не рассчитывается должным образом
Ник Ков
2

Я получил идею из сообщения @Igor и трачу свое время на это для моего проекта с помощью swift

Просто мимо этого в вашем

 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    //do dequeue stuff
    cell.frame = tableView.bounds
    cell.layoutIfNeeded()
    cell.collectionView.reloadData()
    cell.collectionView.heightAnchor.constraint(equalToConstant: cell.collectionView.collectionViewLayout.collectionViewContentSize.height)
    cell.layoutIfNeeded()
    return cell
}

Дополнение: если вы видите, что ваш UICollectionView прерывистый при загрузке ячеек.

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {       
  //do dequeue stuff
  cell.layer.shouldRasterize = true
  cell.layer.rasterizationScale = UIScreen.main.scale
  return cell
}
Назмул Хасан
источник
0

Решение Пабло не очень хорошо сработало для меня, у меня были странные визуальные эффекты (collectionView не настраивается правильно).

Что сработало, так это отрегулировать ограничение высоты collectionView (как NSLayoutConstraint) на collectionView contentSize во время layoutSubviews(). Это метод, который вызывается, когда к ячейке применяется автоматическое размещение.

// Constraint on the collectionView height in the storyboard. Priority set to 999.
@IBOutlet weak var collectionViewHeightConstraint: NSLayoutConstraint!


// Method called by autolayout to layout the subviews (including the collectionView).
// This is triggered with 'layoutIfNeeded()', or by the viewController
// (happens between 'viewWillLayoutSubviews()' and 'viewDidLayoutSubviews()'.
override func layoutSubviews() {

    collectionViewHeightConstraint.constant = collectionView.contentSize.height
    super.layoutSubviews()
}

// Call `layoutIfNeeded()` when you update your UI from the model to trigger 'layoutSubviews()'
private func updateUI() {
    layoutIfNeeded()
}
Фредерик Адда
источник
-3

В вашем UITableViewDelegate:

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return ceil(itemCount/4.0f)*collectionViewCellHeight;
}

Замените itemCount и CollectionViewCellHeight реальными значениями. Если у вас есть массив массивов, itemCount может быть:

self.items[indexPath.row].count

Или как там.

Мартин
источник
Думаю, главный вопрос в том, как рассчитать collectionViewCellHeight до рендеринга.
thesummersign
-3

1.Создайте фиктивную ячейку.
2. Используйте метод collectionViewContentSize в UICollectionViewLayout из UICollectionView, используя текущие данные.

Параг Шинде
источник
Это должен быть лучший ответ ... поскольку нет возможности установить высоту ячейки с помощью 'uicollectionview', не поместив содержимое в фиктивную ячейку, получить высоту, а затем снова установить ее в нужную ячейку
Бартломей Семанчик
как мне использовать этот метод?
xtrinch
Добавьте пример.
Nik Kov
-5

Вы можете рассчитать высоту коллекции на основе его свойств , таких как itemSize, sectionInset, minimumLineSpacing, minimumInteritemSpacing, если ваши collectionViewCellимеют границу правила.

sablib
источник