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