Динамическая высота UICollectionView внутри динамической высоты UITableViewCell
У меня есть горизонтальный UICollectionView
закрепленный на всех краях UITableViewCell
. Элементы в представлении коллекции имеют динамический размер, и я хочу сделать высоту табличного представления равной высоте самой высокой ячейки представления коллекции.
Представления структурированы следующим образом:
UITableView
UITableViewCell
UICollectionView
UICollectionViewCell
UICollectionViewCell
UICollectionViewCell
class TableViewCell: UITableViewCell {
private lazy var collectionView: UICollectionView = {
let collectionView = UICollectionView(frame: self.frame, collectionViewLayout: layout)
return collectionView
}()
var layout: UICollectionViewFlowLayout = {
let layout = UICollectionViewFlowLayout()
layout.estimatedItemSize = CGSize(width: 350, height: 20)
layout.scrollDirection = .horizontal
return layout
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
let nib = UINib(nibName: String(describing: CollectionViewCell.self), bundle: nil)
collectionView.register(nib, forCellWithReuseIdentifier: CollectionViewCellModel.identifier)
addSubview(collectionView)
// (using SnapKit)
collectionView.snp.makeConstraints { (make) in
make.leading.equalToSuperview()
make.trailing.equalToSuperview()
make.top.equalToSuperview()
make.bottom.equalToSuperview()
}
}
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 layout.collectionViewContentSize
}
override func layoutSubviews() {
super.layoutSubviews()
let spacing: CGFloat = 50
layout.sectionInset = UIEdgeInsets(top: 0, left: spacing, bottom: 0, right: spacing)
layout.minimumLineSpacing = spacing
}
}
class OptionsCollectionViewCell: UICollectionViewCell {
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var titleLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
contentView.translatesAutoresizingMaskIntoConstraints = false
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
return contentView.systemLayoutSizeFitting(CGSize(width: targetSize.width, height: 1))
}
}
class CollectionViewFlowLayout: UICollectionViewFlowLayout {
private var contentHeight: CGFloat = 500
private var contentWidth: CGFloat = 200
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
if let attrs = super.layoutAttributesForElements(in: rect) {
// perform some logic
contentHeight = /* something */
contentWidth = /* something */
return attrs
}
return nil
}
override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: contentHeight)
}
}
В моем UIViewController
меня есть tableView.estimatedRowHeight = 500
и tableView.rowHeight = UITableView.automaticDimension
.
Я пытаюсь сделать размер ячейки табличного представления в systemLayoutSizeFitting
равным collectionViewContentSize
, но он вызывался до того, как макет успел установить collectionViewContentSize
на правильное значение. Вот порядок, в котором эти методы вызываются:
cellForRowAt
CollectionViewFlowLayout collectionViewContentSize: (200.0, 500.0) << incorrect/original value
**OptionsTableViewCell systemLayoutSizeFitting (200.0, 500.0) << incorrect/original value**
CollectionViewFlowLayout collectionViewContentSize: (200.0, 500.0) << incorrect/original value
willDisplay cell
CollectionViewFlowLayout collectionViewContentSize: (200.0, 500.0) << incorrect/original value
TableViewCell layoutSubviews() collectionViewContentSize.height: 500.0 << incorrect/original value
CollectionViewFlowLayout collectionViewContentSize: (200.0, 500.0) << incorrect/original value
CollectionViewFlowLayout layoutAttributesForElements
CollectionViewFlowLayout collectionViewContentSize: (450.0, 20.0)
CollectionViewFlowLayout layoutAttributesForElements
CollectionViewFlowLayout collectionViewContentSize: (450.0, 20.0)
CollectionViewCell systemLayoutSizeFitting
CollectionViewCell systemLayoutSizeFitting
CollectionViewCell systemLayoutSizeFitting
CollectionViewFlowLayout collectionViewContentSize: (450.0, 20.0)
CollectionViewFlowLayout layoutAttributesForElements
CollectionViewFlowLayout collectionViewContentSize: (450.0, 301.0) << correct size
CollectionViewFlowLayout collectionViewContentSize: (450.0, 301.0) << correct size
TableViewCell layoutSubviews() collectionViewContentSize.height: 301.0 << correct height
CollectionViewFlowLayout layoutAttributesForElements
CollectionViewFlowLayout collectionViewContentSize: (450.0, 301.0) << correct size
TableViewCell layoutSubviews() collectionViewContentSize.height: 301.0 << correct height
Как можно сделать высоту ячейки моего табличного представления равной самому высокому элементу представления collectionViewContentSize
, представленному макетом collectionViewContentSize
?
Ответы
Ответ 1
Есть несколько шагов для достижения этой цели:
- Определение самоопределения
UICollectionViewCell
- Определение самоопределения
UICollectionView
- Определение собственного размера
UITableViewCell
- Рассчитайте все размеры ячеек и найдите самые высокие (может быть дорого)
- Обновить вид более высокого порядка для любых дочерних
bounds
изменен
Определение размера UICollectionViewCell
Здесь у вас есть два варианта: - использовать авторазметку с автоматически изменяющимися элементами пользовательского интерфейса (такими как UIImageView
или UILabel
или т.д.) - рассчитать и UILabel
размер самостоятельно.
- Использовать автоматическую разметку с саморазмерными элементами пользовательского интерфейса:
если вы установите estimatedItemsSize
быть automatic
и расположение клеток подвиды правильно с элементами selfsizing UI, это будет автоматически проклейки на лету. НО следует учитывать дорогое время компоновки и лаги из-за сложных макетов. Специально на старых устройствах.
- Рассчитайте и укажите его размер самостоятельно:
Одним из лучших мест, где вы можете установить размер ячейки, является intrinsicContentSize
. Хотя для этого есть и другие места (например, в классе flowLayout и т.д.), Здесь вы можете гарантировать, что ячейка не будет конфликтовать с другими саморазмерными элементами пользовательского интерфейса:
override open var intrinsicContentSize: CGSize { return /*CGSize you prefered*/ }
- Определение самоопределения UICollectionView
Любой подкласс UIScrollView
может быть самодостаточным дуэтом для него contentSize
. Так как размеры контента collectionView и tableView постоянно меняются, вы должны сообщить об этом layouter
(официально нигде не известно как Layouter):
protocol CollectionViewSelfSizingDelegate: class {
func collectionView(_ collectionView: UICollectionView, didChageContentSizeFrom oldSize: CGSize, to newSize: CGSize)
}
class SelfSizingCollectionView: UICollectionView {
weak var selfSizingDelegate: CollectionViewSelfSizingDelegate?
override open var intrinsicContentSize: CGSize {
return contentSize
}
override func awakeFromNib() {
super.awakeFromNib()
if let floawLayout = collectionViewLayout as? UICollectionViewFlowLayout {
floawLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
} else {
assertionFailure("Collection view layout is not flow layout. SelfSizing may not work")
}
}
override open var contentSize: CGSize {
didSet {
guard bounds.size.height != contentSize.height else { return }
frame.size.height = contentSize.height
invalidateIntrinsicContentSize()
selfSizingDelegate?.collectionView(self, didChageContentSizeFrom: oldValue, to: contentSize)
}
}
}
Таким образом, после переопределения intrinsicContentSize
мы наблюдаем за изменениями contentSize и сообщаем selfSizingDelegate
что нужно будет знать об этом.
Определение размера UITableViewCell
Эти ячейки могут быть автоматически изменяющимися размерами, реализуя это в их tableView
:
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
Мы поместим selfSizingCollectionView
в эту ячейку и реализуем это:
class SelfSizingTableViewCell: UITableViewCell {
@IBOutlet weak var collectionView: SelfSizingCollectionView!
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
return collectionView.frame.size
}
override func awakeFromNib() {
...
collectionView.selfSizingDelegate = self
}
}
Таким образом, мы соответствуем делегату и реализуем функцию обновления ячейки.
extension SelfSizingTableViewCell: CollectionViewSelfSizingDelegate {
func collectionView(_ collectionView: UICollectionView, didChageContentSizeFrom oldSize: CGSize, to newSize: CGSize) {
if let indexPath = indexPath {
tableView?.reloadRows(at: [indexPath], with: .none)
} else {
tableView?.reloadData()
}
}
}
Обратите внимание, что я написал несколько вспомогательных расширений для UIView
и UITableViewCell
для доступа к представлению родительской таблицы:
extension UIView {
var parentViewController: UIViewController? {
var parentResponder: UIResponder? = self
while parentResponder != nil {
parentResponder = parentResponder!.next
if let viewController = parentResponder as? UIViewController {
return viewController
}
}
return nil
}
}
extension UITableViewCell {
var tableView: UITableView? {
return (next as? UITableView) ?? (parentViewController as? UITableViewController)?.tableView
}
var indexPath: IndexPath? {
return tableView?.indexPath(for: self)
}
}
К этому моменту ваше представление коллекции будет иметь точно необходимую высоту (если оно вертикальное), но вы должны помнить, чтобы ограничить ширину всех ячеек, чтобы она не выходила за пределы ширины экрана.
extension SelfSizingTableViewCell: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: frame.width, height: heights[indexPath.row])
}
}
Заметьте heights
? Здесь вы должны кэшировать (вручную/автоматически) вычисленную высоту всех ячеек, чтобы не отставать. (Я генерирую их все случайно для тестирования)
- Последние два шага:
Последние два шага уже рассмотрены, но вы должны рассмотреть некоторые важные вещи, которые вы ищете:
1 - Autolayout может быть эпическим убийцей производительности, если вы попросите его рассчитать все размеры ячеек для вас, но это надежно.
2 - Вы должны кэшировать все размеры, получить самое высокое и вернуть его в tableView(:,heightForRowAt:)
(больше нет необходимости в самостоятельном tableViewCell
размера tableViewCell
)
3 - Считайте, что самый высокий из них может быть выше, чем весь tableView
и 2D прокрутка будет странной.
4 - Если вы повторно используете ячейку, содержащую collectionView
или tableView
, смещение прокрутки, вставка и многие другие атрибуты ведут себя странно так, как вы этого не ожидаете.
Это один из самых сложных макетов из всех, которые вы могли встретить, но как только вы привыкнете к нему, все станет просто.
Ответ 2
Мне удалось добиться именно того, что вы хотите сделать с помощью ссылки на ограничение высоты для CollectionView (однако я не использую SnapKit, ни programaticUI).
Вот как я это сделал, это может помочь:
- Сначала я регистрирую
NSLayoutConstraint
для UICollectionView, который уже имеет topAnchor
и bottomAnchor
равный его суперпредставлению
![height constraint on UICollectionView inside UITableViewCell]()
class ChatButtonsTableViewCell: UITableViewCell {
@IBOutlet weak var containerView: UIView!
@IBOutlet weak var buttonsCollectionView: UICollectionView!
@IBOutlet weak var buttonsCollectionViewHeightConstraint: NSLayoutConstraint! // reference
}
-
Затем в моей реализации UITableViewDelegate
в cellForRowAt я установил константу ограничения высоты, равную рамке actall collectionView, примерно так:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// let item = items[indexPath.section]
let item = groupedItems[indexPath.section][indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: ChatButtonsTableViewCell.identifier, for: indexPath) as! ChatButtonsTableViewCell
cell.chatMessage = nil // reusability
cell.buttonsCollectionView.isUserInteractionEnabled = true
cell.buttonsCollectionView.reloadData() // load actual content of embedded CollectionView
cell.chatMessage = groupedItems[indexPath.section][indexPath.row] as? ChatMessageViewModelChoicesItem
// >>> Compute actual height
cell.layoutIfNeeded()
let height: CGFloat = cell.buttonsCollectionView.collectionViewLayout.collectionViewContentSize.height
cell.buttonsCollectionViewHeightConstraint.constant = height
// <<<
return cell
}
Я предполагаю, что вы могли бы использовать эту методологию для установки высоты collectionView на высоту самой большой ячейки, заменив let height: CGFloat = cell.buttonsCollectionView.collectionViewLayout.collectionViewContentSize.height
на фактическую let height: CGFloat = cell.buttonsCollectionView.collectionViewLayout.collectionViewContentSize.height
вам высоту.
Кроме того, если вы используете горизонтальный UICollectionView, я предполагаю, что вам даже не нужно изменять эту строку, учитывая, что contentSize.height будет равен вашей самой большой высоте ячейки.
Это работает безупречно для меня, надеюсь, это поможет
РЕДАКТИРОВАТЬ
Просто подумав, вам может понадобиться разместить layoutIfNeeded()
и invalidateLayout()
здесь и там. Если это не сработает сразу, я скажу вам, где я их положил.
РЕДАКТИРОВАТЬ 2
Также забыл упомянуть, что я реализовал willDisplay
и estimatedHeightForRowAt
willDisplay
.
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if let buttonCell = cell as? ChatButtonsTableViewCell {
cellHeights[indexPath] = buttonCell.buttonsCollectionView.collectionViewLayout.collectionViewContentSize.height
} else {
cellHeights[indexPath] = cell.frame.size.height
}
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeights[indexPath] ?? 70.0
}
Ответ 3
У меня была похожая проблема пару дней назад, когда я получил правильную высоту UICollectionView
внутри UITableViewCell
. Ни одно из решений не сработало. Я наконец нашел решение для изменения размера ячеек при повороте устройства
Короче говоря, я обновляю табличное представление с помощью функции:
- (void) reloadTableViewSilently {
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView beginUpdates];
[self.tableView endUpdates];
});
}
Функция вызывается в viewWillAppear
и viewWillTransitionToSize: withTransitionCoordinator
Работает как брелок даже для вращения устройства. Надеюсь это поможет.