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

时间:2019-05-27 01:21:24

标签: swift uitableview uicollectionview

我将水平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 = 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

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

4 个答案:

答案 0 :(得分:4)

有一些步骤可以做到这一点:

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

-定义自定义大小UICollectionViewCell

您在这里有两个选择: -将自动布局与自定义界面元素(例如UIImageViewUILabel等一起使用) -计算并提供您自己的尺寸

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

如果您将estimatedItemsSize设置为automatic,并且使用自定义UI元素正确地布局了单元格子视图,它将自动进行实时调整。 要注意,由于布局复杂,因此布局时间和滞后时间很长。特别是在较旧的设备上。

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

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

override open var intrinsicContentSize: CGSize { return /*CGSize you prefered*/ }

-定义自定义大小UICollectionView

任何UIScrollView子类都可以对其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)
        }
    }
}

因此,在覆盖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
    }
}

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

扩展名SelfSizingTableViewCell:CollectionViewSelfSizingDelegate {         func collectionView(_ collectionView:UICollectionView,didChageContentSizeFrom oldSize:CGSize,to newSize:CGSize){             如果让indexPath = indexPath {                 tableView?.reloadRows(at:[indexPath],带有:.none)             }其他{                 tableView?.reloadData()             }         }     }

请注意,我为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)
    }
}

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

扩展名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-如果您重复使用包含collectionViewtableView的单元格,则滚动偏移量,inset和许多其他属性的行为会超出您的预期。

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

答案 1 :(得分:2)

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

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

  1. 首先,我为UICollectionView注册了一个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

}
  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
    
    }
    

我的假设是,您可以使用此方法将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
}

答案 2 :(得分:1)

它在您的代码中不起作用,因为在systemLayoutSizeFitting完成布局之前调用了单元格的collectionView。为了使其工作,需要在collectionView完成布局后更新表视图的布局。

我写了有关如何self sizing with collection view inside table view cell here的详细说明。我还包括sample codes here。以下是其要点:

  1. 完成收集视图布局时的捕获事件。如果您将UICollectionView子类化,请覆盖layoutSubviews。如果您将UIViewControllerUICollectionViewController子类化,请覆盖viewDidLayoutSubviews
  2. 在收集视图完成布局后,将此事件处理程序连接到表视图单元以更新表视图布局。
  3. 在表格视图单元格中覆盖systemLayoutSizeFitting,以返回集合视图的contentSize,您已经这样做了。
  4. 当您只需要更新其布局(自动高度)时,不要重新加载表格行。我在这里看到一些建议reloadData或reloadRows的答案。如果您的收藏视图很重,请不要这样做。简单的tableView.beginUpdates()tableView.endUpdates()将更新行高,而无需重新加载行。
class CollectionViewController: UICollectionViewController {
    
    //  Set this action during initialization to get a callback when the collection view finishes its layout.
    //  To prevent infinite loop, this action should be called only once. Once it is called, it resets itself
    //  to nil.
    var didLayoutAction: (() -> Void)?
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        
        didLayoutAction?()
        didLayoutAction = nil   //  Call only once
    }
}
class CollectionViewTableViewCell: UITableViewCell {
    
    var collectionViewController: CollectionViewController!

    override func awakeFromNib() {
        super.awakeFromNib()
        
        collectionViewController = CollectionViewController(nibName: String(describing: CollectionViewController.self), bundle: nil)
        
        //  Need to update row height when collection view finishes its layout.
        collectionViewController.didLayoutAction = updateRowHeight
        
        contentView.addSubview(collectionViewController.view)
        collectionViewController.view.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            collectionViewController.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
            collectionViewController.view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
            collectionViewController.view.topAnchor.constraint(equalTo: contentView.topAnchor),
            collectionViewController.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
        ])
    }

    private func updateRowHeight() {
        DispatchQueue.main.async { [weak self] in
            self?.tableView?.updateRowHeightsWithoutReloadingRows()
        }
    }
}
extension UITableView {
    func updateRowHeightsWithoutReloadingRows(animated: Bool = false) {
        let block = {
            self.beginUpdates()
            self.endUpdates()
        }
        
        if animated {
            block()
        }
        else {
            UIView.performWithoutAnimation {
                block()
            }
        }
    }
}

extension UITableViewCell {
    var tableView: UITableView? {
        return (next as? UITableView) ?? (parentViewController as? UITableViewController)?.tableView
    }
}

extension UIView {
    var parentViewController: UIViewController? {
        if let nextResponder = self.next as? UIViewController {
            return nextResponder
        } else if let nextResponder = self.next as? UIView {
            return nextResponder.parentViewController
        } else {
            return nil
        }
    }
}

还要注意,如果您的应用目标是iOS 14,请考虑使用新的UICollectionLayoutListConfiguration。它使集合视图的布局就像表格视图行一样。这是WWDC-20 video,显示了操作方法。

答案 3 :(得分:0)

几天前我也遇到了类似的问题,在$d = new DateTime('2019-06-09T12:56:52.081Z'); $d->setTime($d->format('H'), $d->format('i'), $d->format('s'), $d->format('u') - 1000); echo $d->format('Y-m-d\TH:i:s.u\Z'); // 2019-06-09T12:56:52.080000Z 内获得了UICollectionView的正确高度。这些解决方案均无效。我终于找到了resizing cells on device rotate

的解决方案

简而言之,我使用以下功能更新表格视图:

UITableViewCell

- (void) reloadTableViewSilently { dispatch_async(dispatch_get_main_queue(), ^{ [self.tableView beginUpdates]; [self.tableView endUpdates]; }); } viewWillAppear中调用该函数

即使旋转设备,它也像吊饰一样工作。希望这会有所帮助。