Динамическая высота UICollectionView внутри динамической высоты UITableViewCell - PullRequest
21 голосов
/ 27 мая 2019

У меня есть горизонтальная 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?

Ответы [ 3 ]

17 голосов
/ 01 июня 2019

Есть несколько шагов для достижения этой цели:

  • Определение собственной величины UICollectionViewCell
  • Определение размера UICollectionView
  • Определение собственной величины UITableViewCell
  • Рассчитайте все размеры ячеек и найдите самые высокие (может быть дорого)
  • Перезагрузка высшего порядка для любого ребенка bounds изменено

- Определение размера UICollectionViewCell

У вас есть два варианта: - использовать авторазметку с автоматически изменяющимися элементами пользовательского интерфейса (например, UIImageView или UILabel или т. д.) - посчитайте и предоставьте свой размер

- Использовать авторазметку с автоматически изменяющимися элементами пользовательского интерфейса:

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

- Рассчитайте и укажите его размер:

Одним из лучших мест, где вы можете настроить размер ячейки, является 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)

3 - Считайте, что самый высокий из них может быть выше целого tableView, а 2D прокрутка будет странной.

4 - Если вы повторно используете ячейку, содержащую collectionView или tableView, смещение прокрутки, вставка и многие другие атрибуты ведут себя странно так, как вы этого не ожидаете.

Это один из самых сложных макетов из всех, которые вы могли встретить, но как только вы привыкнете к нему, он станет простым.

8 голосов
/ 29 мая 2019

Мне удалось добиться именно того, что вы хотите сделать с помощью ссылки на ограничение высоты для CollectionView (однако я не использую SnapKit и programaticUI).

Вот как я это сделал, это может помочь:

  1. Сначала я регистрирую 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 на требуемую фактическую высоту.

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

Это работает для меня безупречно, надеюсь, что это поможет

РЕДАКТИРОВАТЬ

Просто подумав, вам может понадобиться разместить layoutIfNeeded() и invalidateLayout()здесь и там.Если это не сработает сразу, я могу сказать вам, куда я их положил.

РЕДАКТИРОВАТЬ 2

Также забыл упомянуть, что я реализовал willDisplay и estimatedHeightForRowAt.

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
}
0 голосов
/ 20 июня 2019

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

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

- (void) reloadTableViewSilently {
   dispatch_async(dispatch_get_main_queue(), ^{
      [self.tableView beginUpdates];
      [self.tableView endUpdates];
   });
}

Функция вызывается в viewWillAppear и viewWillTransitionToSize: withTransitionCoordinator

Работает как брелок даже для вращения устройства.Надеюсь, это поможет.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...