UICollectionView внутри UITableViewCell - динамическая высота?
Один из наших экранов приложений требует от нас UICollectionView
внутри UITableViewCell
, это UICollectionView
будет иметь динамическое количество элементов, в результате чего высота, которая также должна быть рассчитана динамически. Тем не менее, я сталкиваюсь с проблемами, пытаясь вычислить высоту встроенного UICollectionView
,
Наш всеобъемлющий UIViewController
был создан в раскадровке и использует автоматическое расположение. Но я не знаю, как динамически увеличивать высоту UITableViewCell
основанный на высоте UICollectionView
,
Кто-нибудь может дать несколько советов или советов о том, как этого добиться?
17 ответов
Правильный ответ ДА, вы МОЖЕТЕ сделать это.
Я столкнулся с этой проблемой несколько недель назад. Это на самом деле проще, чем вы думаете. Поместите свои ячейки в NIB (или раскадровки) и закрепите их, чтобы позволить авто макету делать всю работу
Учитывая следующую структуру:
TableView
TableViewCell
CollectionView
CollectionViewCell
CollectionViewCell
CollectionViewCell
[... переменное количество ячеек или разные размеры ячеек]
Решение состоит в том, чтобы сказать авторазметке сначала вычислить размеры collectionViewCell, а затем представление viewSize коллекции и использовать его в качестве размера вашей ячейки. Это метод 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, используя оценочный ItemSize UICollectionViewFlowLayout.
Затем хитрость заключается в том, чтобы установить размер ячейки tableView с помощью метода systemLayoutSizeFittingSize.... (ПРИМЕЧАНИЕ: iOS8 или новее)
ПРИМЕЧАНИЕ. Я попытался использовать метод высоты ячейки делегата tableView. -(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
. Но системе автоматического макета уже слишком поздно вычислять ContentViewSize CollectionView, и иногда вы можете найти неправильные ячейки с измененным размером.
@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
Не забудьте включить систему автоматической разметки для ячеек таблицы в вашем 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 помогла разобраться в этом
Я думаю, мой путь намного проще, чем предложенный @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;
Оба представления таблицы и представления коллекции UIScrollView
подклассы и, следовательно, не любят встраиваться в другое представление прокрутки, поскольку они пытаются вычислить размеры контента, повторно использовать ячейки и т. д.
Я рекомендую вам использовать только представление коллекции для всех ваших целей.
Вы можете разделить его на разделы и "рассматривать" макет некоторых разделов как табличное представление, а другие как представление коллекции. В конце концов, нет ничего невозможного в представлении коллекции, которое вы можете получить в представлении таблицы.
Если у вас есть базовая компоновка сетки для "частей" представления вашей коллекции, вы также можете использовать обычные ячейки таблицы для их обработки. Тем не менее, если вам не нужна поддержка iOS 5, вам лучше использовать коллекционные представления.
Ответ Пабло Ромеу выше ( /questions/31081719/uicollectionview-vnutri-uitableviewcell-dinamicheskaya-vyisota/31081727#31081727) очень помог мне в моей проблеме. Я должен был сделать несколько вещей по-другому, однако, чтобы это работало на мою проблему. Во-первых, мне не нужно было звонить 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
в качестве макета для представления вашей коллекции. Наконец, убедитесь, что вы связываете collectionView
IBOutlet на вашу раскадровку.
С пользовательской ячейкой табличного представления выше, я теперь могу разделить ее на подклассы в любой другой ячейке табличного представления, которой нужно представление коллекции, и заставить это реализовать UICollectionViewDelegateFlowLayout
а также UICollectionViewDataSource
протоколы. Надеюсь, что это полезно для кого-то еще!
Я прочитал все ответы. Это, кажется, служит всем случаям.
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
}
Недавно я столкнулся с той же проблемой, и я почти испробовал каждое решение в ответах, некоторые из них работали, а другие не интересовались моим подходом к @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
}
Я думаю, что здесь происходит то, что я заставляю ячейку табличного представления корректировать ее высоту после того, как высота представления коллекции была вычислена. (после предоставления даты collectionView источнику данных)
Я получил идею из сообщения @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
}
Альтернативой решению Pablo Romeu является настройка самого 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()
}
}
Самый простой подход, который я когда-либо придумал, Кредиты на @igor, ответ выше,
В вашем классе tableviewcell просто вставьте это
override func layoutSubviews() {
self.collectionViewOutlet.constant = self.postPoll.collectionViewLayout.collectionViewContentSize.height
}
и, конечно, измените коллекцию viewoutlet с вашей розеткой в классе клетки
Я бы поместил статический метод в класс представления коллекции, который будет возвращать размер, основанный на содержании, которое он будет иметь. Затем используйте этот метод в heightForRowAtIndexPath
вернуть правильный размер.
Также обратите внимание, что вы можете получить странное поведение, когда встраиваете такие виды viewControllers. Я сделал это однажды, и у меня были странные проблемы с памятью, которые я никогда не решал.
Возможно мой вариант будет полезен; Я решал эту задачу в течение последних двух часов. Я не претендую на то, что это на 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
}
}
- Я знаю, что collectionView находится в первом ряду второго раздела;
- Пусть высота ряда составляет 17 с. больше, чем высота его содержимого;
- iconsCellHeight - это случайное число при запуске программы (я знаю, что в портретной форме оно должно быть ровно 392, но это не важно). Если содержимое collectionView + 17 не равно этому числу, измените его значение. В следующий раз в этой ситуации условие дает ЛОЖЬ;
- После всего обновления tableView. В моем случае это сочетание двух операций (для корректного обновления расширяющихся строк);
- И, конечно же, в heightForRowAtIndexPath добавить одну строку в код.
Решение Пабло не сработало для меня, у меня были странные визуальные эффекты (коллекция неправильно настраивалась).
То, что работало, было настроить ограничение высоты collectionView (как NSLayoutConstraint) для collectionView contentSize во время layoutSubviews()
, Этот метод вызывается, когда autolayout применяется к ячейке.
// 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()
}
tempCell.frame = tableView.bounds;
tempCell.layoutIfNeeded()
tempCell.collectionView.reloadData()
tempCell.heightConstForServiceType.constant = tempCell.collectionView.collectionViewLayout.collectionViewContentSize.height + 80.0;`enter code here`
func configure(data: [Strings]) {
names = data
contentView.layoutIfNeeded()
collectionviewNames.reloadData()
}
Коротко и мило. Рассмотрим вышеуказанный метод в своем классе tableViewCell. Вы, вероятно, назвали бы это из
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
после удаления вашего сотового. Перед вызовом reloadData в вашем представлении коллекции в tableCell вам необходимо указать представлению коллекции, чтобы оно размещало свои подпредставления, если обновления макета ожидают.
В вашем UITableViewDelegate:
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return ceil(itemCount/4.0f)*collectionViewCellHeight;
}
Замените itemCount и CollectionViewCellHeight на реальные значения. Если у вас есть массив массивов, itemCount может быть:
self.items[indexPath.row].count
Или что угодно.
1. Создать пустышку.
2. Используйте метод collectionViewContentSize для UICollectionViewLayout UICollectionView, используя текущие данные.
Вы можете рассчитать высоту коллекции на основе ее свойств, таких как itemSize
, sectionInset
, minimumLineSpacing
, minimumInteritemSpacing
, если ваша collectionViewCell имеет границу правила.