Friday, August 31, 2018

How to correctly invalidate layout for supplementary views in UICollectionView

Leave a Comment

I am having a dataset displayed in a UICollectionView. The dataset is split into sections and each section has a header. Further, each cell has a detail view underneath it that is expanded when the cell is clicked.

For reference:

enter image description here

For simplicity, I have implemented the details cells as standard cells that are hidden (height: 0) by default and when the non-detail cell is clicked, the height is set to non-zero value. The cells are updates using invalidateItems(at indexPaths: [IndexPath]) instead of reloading cells in performBatchUpdates(_ updates: (() -> Void)?, completion: ((Bool) -> Void)? = nil) as the animations seems glitchy otherwise.

Now to the problem, the invalidateItems function obviously updates only cells, not supplementary views like the section header and therefore calling only this function will result in overflowing the section header:

enter image description here

After some time Googling, I found out that in order to update also the supplementary views, one has to call invalidateSupplementaryElements(ofKind elementKind: String, at indexPaths: [IndexPath]). This might recalculate the section header's bounds correctly, however results in the content not appearing:

enter image description here

This is most likely caused due to the fact that the func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView does not seem to be called.

I would be extremely grateful if somebody could tell me how to correctly invalidate supplementary views to the issues above do not happen.

Code:

   override func numberOfSections(in collectionView: UICollectionView) -> Int {         return dataManager.getSectionCount()     }      override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {         let count = dataManager.getSectionItemCount(section: section)         reminder = count % itemsPerWidth         return count * 2     }      override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {          if isDetailCell(indexPath: indexPath) {             let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Reusable.CELL_SERVICE, for: indexPath) as! ServiceCollectionViewCell             cell.lblName.text = "Americano detail"              cell.layer.borderWidth = 0.5             cell.layer.borderColor = UIColor(hexString: "#999999").cgColor             return cell          } else {             let item = indexPath.item > itemsPerWidth ? indexPath.item - (((indexPath.item / itemsPerWidth) / 2) * itemsPerWidth) : indexPath.item             let product = dataManager.getItem(index: item, section: indexPath.section)              let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Reusable.CELL_SERVICE, for: indexPath) as! ServiceCollectionViewCell             cell.lblName.text = product.name              cell.layer.borderWidth = 0.5             cell.layer.borderColor = UIColor(hexString: "#999999").cgColor              return cell         }     }      override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {         switch kind {         case UICollectionElementKindSectionHeader:             if indexPath.section == 0 {                 let header = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: Reusable.CELL_SERVICE_HEADER_ROOT, for: indexPath) as! ServiceCollectionViewHeaderRoot                 header.lblCategoryName.text = "Section Header"                 header.imgCategoryBackground.af_imageDownloader = imageDownloader                 header.imgCategoryBackground.af_setImage(withURLRequest: ImageHelper.getURL(file: category.backgroundFile!))                 return header             } else {                 let header = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: Reusable.CELL_SERVICE_HEADER, for: indexPath) as! ServiceCollectionViewHeader                 header.lblCategoryName.text = "Section Header"                 return header             }         default:             assert(false, "Unexpected element kind")         }     }      // MARK: UICollectionViewDelegate      func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {         let width = collectionView.frame.size.width / CGFloat(itemsPerWidth)          if isDetailCell(indexPath: indexPath) {             if expandedCell == indexPath {                 return CGSize(width: collectionView.frame.size.width, height: width)             } else {                 return CGSize(width: collectionView.frame.size.width, height: 0)             }         } else {             return CGSize(width: width, height: width)         }     }      func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {         if section == 0 {             return CGSize(width: collectionView.frame.width, height: collectionView.frame.height / 3)         } else {             return CGSize(width: collectionView.frame.width, height: heightHeader)         }     }      override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {         if isDetailCell(indexPath: indexPath) {             return         }          var offset = itemsPerWidth         if isLastRow(indexPath: indexPath) {             offset = reminder         }          let detailPath = IndexPath(item: indexPath.item + offset, section: indexPath.section)         let context = UICollectionViewFlowLayoutInvalidationContext()          let maxItem = collectionView.numberOfItems(inSection: 0) - 1         var minItem = detailPath.item         if let expandedCell = expandedCell {             minItem = min(minItem, expandedCell.item)         }          // TODO: optimize this         var cellIndexPaths = (0 ... maxItem).map { IndexPath(item: $0, section: 0) }          var supplementaryIndexPaths = (0..<collectionView.numberOfSections).map { IndexPath(item: 0, section: $0)}          for i in indexPath.section..<collectionView.numberOfSections {             cellIndexPaths.append(contentsOf: (0 ... collectionView.numberOfItems(inSection: i) - 1).map { IndexPath(item: $0, section: i) })             //supplementaryIndexPaths.append(IndexPath(item: 0, section: i))         }          context.invalidateSupplementaryElements(ofKind: UICollectionElementKindSectionHeader, at: supplementaryIndexPaths)         context.invalidateItems(at: cellIndexPaths)          if detailPath == expandedCell {             expandedCell = nil         } else {             expandedCell = detailPath         }          UIView.animate(withDuration: 0.25) {             collectionView.collectionViewLayout.invalidateLayout(with: context)             collectionView.layoutIfNeeded()         }     } 

1 Answers

Answers 1

If you call your collectionView.layoutIfNeeded() within your animate function the animation doesn't work. I suggest you reload the collectionView as you should have invalidated the supplement views it should give you the required results. If however that will not work then you can try adding a couple of print statements to see if the invalidatedSupplementViews are registered correctly.

// TODO: optimize this     var cellIndexPaths = (0 ... maxItem).map { IndexPath(item: $0, section: 0) }      var supplementaryIndexPaths = (0..<collectionView.numberOfSections).map { IndexPath(item: 0, section: $0)}      for i in indexPath.section..<collectionView.numberOfSections {         cellIndexPaths.append(contentsOf: (0 ... collectionView.numberOfItems(inSection: i) - 1).map { IndexPath(item: $0, section: i) })         //supplementaryIndexPaths.append(IndexPath(item: 0, section: i))     }      context.invalidateSupplementaryElements(ofKind: UICollectionElementKindSectionHeader, at: supplementaryIndexPaths)     context.invalidateItems(at: cellIndexPaths)     print("____________INFO________________")     print("THE SUPPLEMENTARY INDEX PATHS ARE: \(supplementaryIndexPaths))"     print("------CONTEXT-----")     print("\(context)")     print("__________END OF DEBUG INFO_________")      if detailPath == expandedCell {         expandedCell = nil     } else {         expandedCell = detailPath     }      UIView.animate(withDuration: 0.25) {         collectionView.collectionViewLayout.invalidateLayout(with: context)     }     collectionView.layoutIfNeeded() //if this doesn't work then try uncommenting the next line     //collectionView.reloadData()   } 

If all this will not help then paste what you get in those debug print statements. Or you could share the code on github I could try to contribute.

EDIT 1:

// MARK: UICollectionViewDelegate //add @objc to the func below @objc func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {     let width = collectionView.frame.size.width / CGFloat(itemsPerWidth)      if isDetailCell(indexPath: indexPath) {         if expandedCell == indexPath {             return CGSize(width: collectionView.frame.size.width, height: width)         } else {             return CGSize(width: collectionView.frame.size.width, height: 0)         }     } else {         return CGSize(width: width, height: width)     } } 
If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment