动态高度UITableViewCell内的动态高度UICollectionView

chi*_*uda 21 uitableview ios uicollectionview swift

我将水平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)
    }
}
Run Code Online (Sandbox Code Playgroud)

在我身上UIViewController,我有tableView.estimatedRowHeight = 500tableView.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
Run Code Online (Sandbox Code Playgroud)

如何使表格视图单元格的高度等于集合视图的最高项目(由布局的代表)的高度collectionViewContentSize

Moj*_*ini 17

有一些步骤可以实现此目的:

  • 定义自定义大小 UICollectionViewCell
  • 定义自定义大小 UICollectionView
  • 定义自定义大小 UITableViewCell
  • 计算所有像元大小并找到最高像元(可能会很贵)
  • 重新加载任何孩子已bounds更改的高阶视图

-定义自定义大小 UICollectionViewCell

您在这里有两个选择:-对自动调整尺寸的UI元素(如UIImageViewor UILabel或or等)使用自动布局-计算并提供您自己的尺寸

- 将自动布局与自定义界面元素一起使用:

如果使用自定尺寸的UI元素正确设置estimatedItemsSizebe automatic和布局单元格子视图,它将自动进行实时定尺寸。但是要注意昂贵的布局时间和滞后,因为布局复杂。特别是在较旧的设备上。

- 计算并提供您自己的尺寸:

可以设置像元大小的最佳位置之一是intrinsicContentSize。尽管还有其他地方可以执行此操作(例如,在flowLayout类等中),但是可以保证单元格不会与其他自定义UI元素发生冲突:

override open var intrinsicContentSize: CGSize { return /*CGSize you prefered*/ }
Run Code Online (Sandbox Code Playgroud)

-定义自定义大小 UICollectionView

所有UIScrollView子类可以selfsizing二人到它的contentSize。由于collectionView和tableView内容的大小在不断变化,因此您应该通知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)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

因此,在覆盖之后,intrinsicContentSize我们观察contentSize的变化,并告知selfSizingDelegate需要对此进行了解。

-定义自定义大小 UITableViewCell

通过在以下单元中实现此单元,可以自定义大小tableView

override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    return UITableView.automaticDimension
}
Run Code Online (Sandbox Code Playgroud)

我们将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
    }
}
Run Code Online (Sandbox Code Playgroud)

因此,我们符合委托并实现更新单元格的功能。

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()
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,我为UIViewUITableViewCell访问父表视图编写了一些帮助程序扩展:

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)
    }
}
Run Code Online (Sandbox Code Playgroud)

至此,您的集合视图将具有恰好需要的高度(如果它是垂直的),但是您应该记住限制所有单元格的宽度,以免超出屏幕宽度。

extension SelfSizingTableViewCell: UICollectionViewDelegateFlowLayout {

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: frame.width, height: heights[indexPath.row])
    }
}
Run Code Online (Sandbox Code Playgroud)

通知heights吗?在这里,您应该缓存(手动/自动)所有单元格的计算高度,以免延迟。(出于测试目的,我会随机生成所有这些文件)

-最后两个步骤:

已经涵盖了最后两个步骤,但是您应该考虑有关所寻求案例的一些重要事项:

1-如果您要求自动布局为您计算所有单元格大小,它可能是史诗般的性能杀手,但这是可靠的。

2-您必须缓存所有大小并获得最高的大小并将其返回tableView(:,heightForRowAt:)(不再需要调整大小tableViewCell

3-考虑到最高的可能比整个高,tableView而2D滚动会很奇怪。

4-如果您重复使用包含collectionView或的单元格tableView,则滚动偏移量,插入量和许多其他属性的行为会与您期望的方式不同。

这是您可能遇到的最复杂的布局之一,但是一旦习惯了它,它就会变得很简单。


Oly*_*tre 8

我设法通过CollectionView的高度约束参考(不过我不使用SnapKit,也不使用programaticUI)来实现您想要的功能。

这是我的做法,可能会有所帮助:

  1. 首先,我NSLayoutConstraint为UICollectionView 注册了一个,该UICollectionView已经有一个topAnchorbottomAnchor等于其超级视图

UITableViewCell内的UICollectionView的高度限制

class ChatButtonsTableViewCell: UITableViewCell {

    @IBOutlet weak var containerView: UIView!
    @IBOutlet weak var buttonsCollectionView: UICollectionView!
    @IBOutlet weak var buttonsCollectionViewHeightConstraint: NSLayoutConstraint! // reference

}
Run Code Online (Sandbox Code Playgroud)
  1. 然后在我的UITableViewDelegate实现中,在cellForRowAt上,将高度约束的常量设置为与实际的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
    
    }
    
    Run Code Online (Sandbox Code Playgroud)

我的假设是,您可以使用此方法将collectionView的高度设置为最大单元格的高度,let height: CGFloat = cell.buttonsCollectionView.collectionViewLayout.collectionViewContentSize.height而不是所需的实际高度。

另外,如果您使用的是水平UICollectionView,考虑到contentSize.height将等于您的最大单元格高度,则您甚至不必更改此行。

它对我来说完美无缺,希望对您有所帮助

编辑

只是在想,你可能需要放置一些layoutIfNeeded()invalidateLayout()在这里和那里。如果无法立即使用,我可以告诉您我将其放在哪里。

编辑2

还忘了提到我实现了willDisplayestimatedHeightForRowAt

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
}
Run Code Online (Sandbox Code Playgroud)