3

So I've been trying to have a collection view with custom FlowLayout to adapt its own height based on the UICollectionViewCells it contains. I've been searching online a lot and all the solutions I found led to different exceptions being raised during runtime.

One thing to know is that my collectionview sits inside a tableview cell. The tableview cells' height changes accordingly when their contents need more/less space, but the collectionview they contain doesn't....

Most of the time, the collectionview in question has one or two items, and their width makes it so that they fit on a single row. But sometimes, the items are too wide and therefore need to be wrapped on top of each other.

When that happens, the collectionview's height should expand to display both rows of items, but that doesn't happen.

Some information:

Nib file How the collectionview is implemented

Nib file's awakeFromNib() The tableview cell's awakeFromNib

class BaseOnboardingTableViewCell: UITableViewCell {

@IBOutlet weak var watsonIndicator: UIView!
@IBOutlet weak var cellTextLabel: UILabel!
@IBOutlet weak var customContentViewContainer: UIView!
@IBOutlet weak var actionButtonsCollection: UICollectionView!

@IBOutlet var customContentViewContainerHeightConstraint: NSLayoutConstraint!
@IBOutlet var actionButtonsCollectionHeightConstraint: NSLayoutConstraint!

@IBOutlet var customContentDistanceFromCellTextLabelConstraint: NSLayoutConstraint!
@IBOutlet var actionButtonsCollectionDistanceFromCustomContentConstraint: NSLayoutConstraint!

/// Keeps track wether the cell has animated for its intro or not
var hasAnimated: Bool = false

/// Receiver for interaction with chat actions
var chatActionDelegate: OnboardingChatActionDelegate?

/// Dictates what the cell should contain and display
var onboardingStep: OnboardingStep! {
    didSet {
        if onboardingStep.text() != nil {
            cellTextLabel.text = onboardingStep.text()!
        } else {
            hideText()
        }

        if onboardingStep.customContent != nil {
            loadCustomContent()
        } else {
            hideCustomContent()
        }

        if onboardingStep.chatActions == nil {
            hideActionButtonsCollection()
        } else {
            actionButtonsCollection.collectionViewLayout.invalidateLayout()
            actionButtonsCollection.reloadData()
            actionButtonsCollectionHeightConstraint.constant = actionButtonsCollection.contentSize.height
        }
    }
}


/// Prepares the cell before setting its OnboardingStep
override func awakeFromNib() {
    super.awakeFromNib()

    self.translatesAutoresizingMaskIntoConstraints = true

    let collectionController = OnboardingChatActionCollectionViewController()
    collectionController.attach(toCollection: actionButtonsCollection)
    collectionController.onboardingCellDelegate = self

    actionButtonsCollection.sizeToFit()
    watsonIndicator.layer.cornerRadius = watsonIndicator.frame.width/2
    makeAllTransparent()
}

The CollectionView's controller The collectionview's controller

class OnboardingChatActionCollectionViewController: UICollectionViewFlowLayout, UICollectionViewDataSource, UICollectionViewDelegate {

var onboardingCellDelegate: BaseOnboardingTableViewCell?


/// This will link a UICollectionView to this controller
func attach(toCollection collection: UICollectionView) {
    collection.delegate = self
    collection.dataSource = self
    collection.collectionViewLayout = self

    collection.contentInset.left = 0
    collection.contentInset.right = 0

    collection.backgroundColor = UIColor.clear

    self.estimatedItemSize = CGSize(width: 50, height: 29)


    collection.register(UINib(nibName: "ChatActionButton", bundle: nil), forCellWithReuseIdentifier: "ChatActionButtonCell")
}


/// Renders buttons inactive and greys them out (based on user selection) after an action was tapped
fileprivate func greyOutButtons(selectedCell: OnboardingChatActionCollectionViewCell) {
    DispatchQueue.main.async {

        let cells = self.collectionView!.visibleCells as! [OnboardingChatActionCollectionViewCell]

        for cell in cells {
            if cell == selectedCell {
                cell.background.backgroundColor = UIColor(colorLiteralRed: 52/255, green: 51/255, blue: 52/255, alpha: 1)
            } else {
                cell.background.backgroundColor = UIColor.aqua10
                cell.actionButton.setTitleColor(UIColor.gunmetal, for: .normal)
            }

            cell.actionButton.isEnabled = false
        }
    }
}


/// Wrapping buttons on a new row if they're too large
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    let attributes = super.layoutAttributesForElements(in: rect)

    var leftMargin = sectionInset.left
    var maxY: CGFloat = -1.0
    attributes?.forEach { layoutAttribute in
        if layoutAttribute.frame.origin.y >= maxY {
            leftMargin = sectionInset.left
        }

        layoutAttribute.frame.origin.x = leftMargin

        leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
        maxY = max(layoutAttribute.frame.maxY , maxY)
    }

    return attributes
}


func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return onboardingCellDelegate?.onboardingStep.chatActions?.count ?? 0
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let chatActionCell = self.collectionView!.dequeueReusableCell(withReuseIdentifier: "ChatActionButtonCell", for: indexPath) as! OnboardingChatActionCollectionViewCell

    chatActionCell.chatAction = onboardingCellDelegate!.onboardingStep.chatActions![indexPath.row]

    chatActionCell.background.layer.cornerRadius = chatActionCell.background.frame.height / 2
    chatActionCell.sizeToFit()

    chatActionCell.collectionControllerDelegate = self

    return chatActionCell
}
Skwiggs
  • 1,348
  • 2
  • 17
  • 42
  • You probably want to create a height constraint, and then update it when the collection view's content size changes. Take a look at this post: https://stackoverflow.com/a/20829728/6257435 – DonMag Aug 23 '17 at 12:58
  • @DonMag I've actually done that, and trying to set its height to `collectionView.collectionViewLayout.collectionViewContentSize.height`, but that always returns the height for a single row even if items are re-flowed on more rows... – Skwiggs Aug 23 '17 at 13:15
  • Sure your collection view flow is set to Vertical? – DonMag Aug 23 '17 at 13:22
  • And... are you making sure you're getting it *after* the content has fully reloaded? – DonMag Aug 23 '17 at 13:23
  • @DonMag Not sure what you mean with setting the flow to Vertical. I need items to be laid out horizontally when not too large, and flow to a new row when they can't all be fit on a single row. For the rest, making sure I'm getting it (what ?) after the content has fully reloaded, I don't know. I'm lost as I'm having to make everything as modular as possible and that means each table cell has a collection with variable amount of items (which can all have different widths)... – Skwiggs Aug 23 '17 at 13:28
  • Hmmm... from Apple's docs on `UICollectionViewLayout`: *"Subclasses must override this method..."* [link](https://developer.apple.com/documentation/uikit/uicollectionviewlayout/1617796-collectionviewcontentsize) - maybe that's why you're not getting an accurate height? Vertical flow means "wrap onto rows and scroll vertically", which is what you want --- you'll just be resizing the View instead of letting it scroll. – DonMag Aug 23 '17 at 14:06
  • It does look like I am indeed doing this too early. I overwrote `collectionViewContentSize` and it is indeed returning 0 as it hasn't had time to go through the layout phase. – Skwiggs Aug 23 '17 at 14:43

0 Answers0