UICollectionView will recreate cells when calling the "reloadItems" method - ios

My cell has an image which be downloaded from network, therefore I need to set the height of cell as dynamic.
When an image download is finished, I am going to call self.collectionView.reloadItems(at: [indexPath]) to trigger the delegate method for setting a new height.
But it seems that the reloadItems method will recreate a cell, not just re-layout an original reuse cell.
How can I solve this problem? Is it a bug on UICollectionView from apple or something wrong I did?
Whole code:
// code from ViewController
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: AnnounmentWallCollectionViewCell.cellIdentifier, for: indexPath) as! AnnounmentWallCollectionViewCell
let announcement = announcements[indexPath.row]
cell.collectionView = collectionView
cell.setBanner(from: announcement.banner, indexPath: indexPath, completion: { [unowned self] (height) in
self.bannersHeight[indexPath.row] = height
})
cell.setHTMLContent(announcement.content)
contentsHeight[indexPath.row] = cell.htmlContentSize.height
printD("indexPath: \(indexPath)")
return cell
}
// code from cell
func setBanner(from url: URL?, indexPath: IndexPath, completion: #escaping (_ height: CGFloat)->()) {
// URL(string: "https://i.imgur.com/qzY7BJ9.jpg")
if let url = url {
if let banner = SDImageCache.shared().imageFromDiskCache(forKey: url.absoluteString) {
self.bannerView.isHidden = false
self.bannerView.image = banner.scaleWidth(to: self.bounds.width - 32) // leading + trailling
self.bannerHeight.constant = self.bannerView.image?.size.height ?? 1
completion(self.bannerHeight.constant)
printD("NO Download: \(indexPath)")
let animationsEnabled = UIView.areAnimationsEnabled
UIView.setAnimationsEnabled(false)
self.collectionView.reloadItems(at: [indexPath])
UIView.setAnimationsEnabled(animationsEnabled)
} else {
DispatchQueue.global().async {
SDWebImageDownloader.shared().downloadImage(with: url, options: .useNSURLCache, progress: nil) { (banner, data, error, finished) in
DispatchQueue.main.async {
if let banner = banner {
SDImageCache.shared().store(banner, forKey: url.absoluteString, toDisk: true)
self.bannerView.isHidden = false
self.bannerHeight.constant = banner.scaleWidth(to: self.bounds.width - 32)?.size.height ?? 1
completion(self.bannerHeight.constant)
self.collectionView.reloadData()
printD("Download: \(indexPath): \(self.bannerHeight.constant)")
} else {
self.bannerView.isHidden = true
self.bannerHeight.constant = 1
completion(self.bannerHeight.constant)
}
}
}
}
}
} else {
bannerView.isHidden = true
bannerHeight.constant = 1
completion(bannerHeight.constant)
}
}
// code from delegate
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = self.view.bounds.width
let height = bannersHeight[indexPath.row] + contentsHeight[indexPath.row]
+ 1 // sticker
+ 11 // banner top
printD("indexPath: \(indexPath): \(height)")
return CGSize(width: width, height: height)
}

That's not a bug. That's how you reload a cell for a given index path. If you only want to update the layout you can also try
[self.collectionView.collectionViewLayout invalidateLayout]
instead of
self.collectionView.reloadItems(at: [indexPath])
and then return the proper size in the delegate method.
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
return //whatever size that you want to return.
}
I would also highly suggest you to cache your image sizes so that you can use that for the next time, instead of downloading/calculating over an over...

Related

Image on CollectionView cell changes when scrolling

I have a collectionView that has custom cells. Each custom cell has an image in it and other components. The problem I'm having is that when I scroll, the images changes to another images from other cells on the collectionView. I'm gonna put the code that I'm using so it's more clear to see how I'm implementing the collectionView and it's cell.
func registerCell() {
collectionView.register(PageCell.self, forCellWithReuseIdentifier: cellId)
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let pageNumber = Int(targetContentOffset.pointee.x / view.frame.width)
pageControl.currentPage = pageNumber
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return feedTitles.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! PageCell
cell.titleLabel.text = self.feedTitles[indexPath.item].uppercased()
cell.textView.text = self.feedDescription[indexPath.item]
guard let imageURL = URL(string: self.feedImages[indexPath.item]) else {return cell}
cell.imageView.load(url: imageURL)
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.frame.width, height: collectionView.frame.height)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
This is the code for getting the images from a server:
var feeds: [Feed]?
func getFeed(){
feedService.getFeed() { [weak self](feedRes) in
switch feedRes {
case .success(let successResponse):
print("Success Feed")
if successResponse.data.count < 1 {
} else{
self?.feeds = successResponse.data
self?.feedTitles = self?.feeds?.compactMap({ $0.title
}) ?? ["Titulo Noticia"]
self?.feedImages = self?.feeds?.compactMap({ $0.image
}) ?? ["Imagen"]
self?.feedDescription = self?.feeds?.compactMap({ $0.content
}) ?? ["Descripcion Imagen"]
self?.collectionView.reloadData()
self?.pageControl.numberOfPages = (self?.feedTitles.count)!
self?.pageControl.isHidden = false
}
case .failure(let feedError):
print("Fail Feed \(feedError)")
print(feedError.localizedDescription)
self?.pageControl.isHidden = true
}
}
}
This is the "load" function:
func load(url: URL) {
DispatchQueue.global().async { [weak self] in
if let data = try? Data(contentsOf: url) {
if let image = UIImage(data: data) {
DispatchQueue.main.async {
self?.image = image
}
}
}
}
}
The issue is inside your cell.imageView.load(url: imageURL) implementation.
Downloading an image is an asynchronous operation. When you create a cell - you start an operation and you don't know when it's going to complete. Whenever it completes, it sets the image on your UIImageView instance.
The cell on the other hand is a reuseable component for collectionView and when you scroll, same cell instance that disappeared by going out of screen from top will also reappear by coming in from bottom of the screen. This means that one cell instance can trigger multiple image download requests and it doesn't cancel it's previous in-flight image download request. It also does not know which image download call completed - the last one (or the one before that etc.)
What you need to do is - add some additional protection in the imageView load method so that when a cell (and hence imageView) instance is reused - it only sets the image for last request (not the ones done prior to the last one).
Copy paste following code into your project.
import UIKit
import ObjectiveC.runtime
private var keyTagIdentifier: String?
public extension UIView {
var tagIdentifier: String? {
get { return objc_getAssociatedObject(self, &keyTagIdentifier) as? String }
set { objc_setAssociatedObject(self, &keyTagIdentifier, newValue, .OBJC_ASSOCIATION_RETAIN) }
}
}
Update your image load extension like this
public extension UIImageView {
func load(url: URL) {
// 1. Assign the current request identifier on this instance
let lastRequestIdentifier = url.absoluteString
self.tagIdentifier = lastRequestIdentifier
// 2. Download the image as you are doing today
// 3. After the image has been downloaded and you set the image
// Please check whether we are still interested in same image or not
// This will be written inside your image download completion block
if self.tagIdentifier == lastRequestIdentifier {
self.image = // the image you downloaded
}
}
}
I believe the issue is due to UICollectionView reusing cells. Meaning the cell that disappears from the top, is rendered at the bottom, usually with the same state as the cell that disappeared from the top and vice-versa for the cells at the bottom. You can avoid this behaviour by adding the following to you code to your PageCell,
override func prepareForReuse() {
super.prepareForReuse()
imageView?.image = nil
}

Update UICollectionView height based on content

UICollectionView height not updating properly based on content. How to update height
My code :
collectionViewArray.append(trimmedStrig!)
DispatchQueue.main.async {
if self.collectionViewArray.count == 1 {
self.collectionViewHeight.constant = self.collectionView.contentSize.height + 50
print(self.collectionViewHeight.constant)
} else if self.collectionViewArray.count > 1 {
self.collectionViewHeight.constant = self.collectionView.contentSize.height
print(self.collectionViewHeight.constant)
}
self.collectionView.reloadData()
}
//Fix collection view cell height and width
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: 100, height: 50)
}
When create 1st cell it's not displaying that cell, that's why I added 50 and when cell entered into second row again it's not updating height, but when 2nd row 2nd cell created in that Time height updated. Why I don't know?
Try this one its may be helpful for you
self.collView.reloadData()
self.collView.performBatchUpdates({}) { (complition) in
self.cnsHeightCollView.constant = self.collView.contentSize.height
}
Add this line in code:-
collectionView.translatesAutoresizingMaskIntoConstraints = true
e.g.
override func viewDidAppear(_ animated: Bool)
{
super.viewDidAppear(animated)
if self.collectionViewArray.count == 1
{
self.collectionViewHeight.constant = self.collectionView.contentSize.height + 50
print(self.collectionViewHeight.constant)
}
else if self.collectionViewArray.count > 1
{
self.collectionViewHeight.constant = self.collectionView.contentSize.height
print(self.collectionViewHeight.constant)
}
collectionView.translatesAutoresizingMaskIntoConstraints = true //Here
self.collectionView.reloadData()
}

Single CollectionView with multiple CollectionViewLayout.why Layout is mess up?

I have Single UICollectionView , and I want to Apply Two different layout dynamically.
UICollectionViewFlowLayout : A Layout with same size cell and circle image.
var flowLayout: UICollectionViewFlowLayout {
let flowLayout = UICollectionViewFlowLayout()
flowLayout.itemSize = CGSize(width: UIScreen.main.bounds.width/3, height: 140)
flowLayout.sectionInset = UIEdgeInsetsMake(0, 0, 0, 0)
flowLayout.scrollDirection = UICollectionViewScrollDirection.vertical
flowLayout.minimumInteritemSpacing = 0.0
return flowLayout
}
Pintrest Layout :
https://www.raywenderlich.com/392-uicollectionview-custom-layout-tutorial-pinterest
For Example : when user Click on Profile Button FlowLayout will be Apllied and Cell Appear with image in Circle Shape. when user click on Picture button pintrest layout will be Applied and cell Appear with image in Rectangle shape with dynamic height.
intially CollectionView have 1.flowLayout and it appears perfectly.but when I click on Picture button Pintrest layout is messed up with previous layout as shown in above image.
Following is Code For changing Layout.
if isGrid {
let horizontal = flowLayout
recentCollectionView.setCollectionViewLayout(horizontal, animated: true)
recentCollectionView.reloadData()
}
else {
let horizontal = PinterestLayout()
horizontal.delegate = self
recentCollectionView.setCollectionViewLayout(horizontal, animated: true)
recentCollectionView.reloadData()
}
ViewHiarchy:
I have main Collection-view that Contain header and one bottom cell.cell contain other Collection-view to which I am Applying multiple layout.I have Two Different cell for each layout.I want bottom cell size equal to content Collection-view Content size so user can Scroll entire main collection-view vertically.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
var cell : UICollectionViewCell!
switch isGrid {
case true:
cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SearchProfileCell", for: indexPath)
if let annotateCell = cell as? SearchProfileCell {
annotateCell.photo = photos[indexPath.item]
}
case false:
cell = collectionView.dequeueReusableCell(withReuseIdentifier: "AnnotatedPhotoCell", for: indexPath)
if let annotateCell = cell as? AnnotatedPhotoCell {
annotateCell.cellwidth = collectionView.contentSize.width/3
annotateCell.photo = photos[indexPath.item]
}
}
cell.contentView.layer.cornerRadius = 0
return cell
}
Code of profile and picture button Action.
#IBAction func pictureClick(sender:UIButton) {
isGrid = false
self.searchCollectionView.reloadData()
}
#IBAction func profilClick(sender:UIButton) {
isGrid = true
self.searchCollectionView.reloadData()
}
I think problem is not inside layout but might be inside cellForItemAt. if you are using different cell for both layout then do not compare bool at cellForItemAt method. you should compare layout class type
like below code :
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if collectionView.collectionViewLayout.isKind(of: PinterestLayout.self) {
// return cell for PinterestLayout
guard let annotateCell = collectionView.dequeueReusableCell(withReuseIdentifier: "SearchProfileCell", for: indexPath) as? SearchProfileCell else {
fatalError("SearchProfileCell Not Found")
}
annotateCell.photo = photos[indexPath.item]
return annotateCell
} else {
// return cell for flowLayout
guard let annotateCell = collectionView.dequeueReusableCell(withReuseIdentifier: "AnnotatedPhotoCell", for: indexPath) as? AnnotatedPhotoCell else {
fatalError("AnnotatedPhotoCell Not Found")
}
annotateCell.cellwidth = collectionView.contentSize.width/3
annotateCell.photo = photos[indexPath.item]
return annotateCell
}
}
Also need to update layout change action methods like:
#IBAction func pictureClick(sender:UIButton) {
isGrid = false
self.collectionView?.collectionViewLayout.invalidateLayout()
self.collectionView?.setCollectionViewLayout(PinterestLayout(),
animated: false, completion: { [weak self] (complite) in
guard let strongSelf = self else {
return
}
strongSelf.searchCollectionView?.reloadData()
})
}
#IBAction func profilClick(sender:UIButton) {
isGrid = true
self.collectionView?.collectionViewLayout.invalidateLayout()
self.collectionView?.setCollectionViewLayout(flowLayout,
animated: false, completion: { [weak self] (complite) in
guard let strongSelf = self else {
return
}
strongSelf.searchCollectionView?.reloadData()
})
}
Why you are using two different layout even though you can achieve same Result with pintrestLayout. https://www.raywenderlich.com/392-uicollectionview-custom-layout-tutorial-pinterest.
Check pintrestLayout carefully , it have Delegate for Dynamic height.
let photoHeight = delegate.collectionView(collectionView, heightForPhotoAtIndexPath: indexPath)
if you return static height here , your pintrest layout become GridLayout(your First Layout).
if you want pintrest layout as work for both layout , you need to declare same Boolean(isGrid) in pintrest layout.and use this boolean to return UICollectionViewLayoutAttributes
more important raywenderlich pintrest layout uses cache to store layout attribute.you have to remove cache object before applying other layout.
Check in this tutorial , how same layout used for grid,list and linear.
https://benoitpasquier.com/optimise-uicollectionview-swift/
what you need in your layout.
var isGrid : Bool = true {
didSet {
if isGrid != oldValue {
cache.removeAll()
self.invalidateLayout()
}
}
}

How to correctly invalidate layout for supplementary views in UICollectionView

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:
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:
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:
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()
}
}
EDIT:
Minimalistic project demonstrating this issue: https://github.com/vongrad/so-expandable-collectionview
You should use an Invalidation Context. It's a bit complex, but here's a rundown:
First, you need to create a custom subclass of UICollectionViewLayoutInvalidationContext since the default one used by most collection views will just refresh everything. There may be situations where you DO want to refresh everything though; in my instance, if the width of the collection view changes it has to layout all the cells again, so my solution looks like this:
class CustomInvalidationContext: UICollectionViewLayoutInvalidationContext {
var justHeaders: Bool = false
override var invalidateEverything: Bool { return !justHeaders }
override var invalidateDataSourceCounts: Bool { return false }
}
Now you need to tell the layout to use this context instead of the default:
override class var invalidationContextClass: AnyClass {
return CustomInvalidationContext.self
}
This won't trigger if we don't tell the layout it needs to update upon scrolling, so:
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
I'm passing true here because there will always be something to update when the user scrolls the collection view, even if it's only the header frames. We'll determine exactly what gets changed when in the next section.
Now that it is always updating when the bounds change, we need to provide it with information about which parts should be invalidated and which should not. To make this easier, I have a function called getVisibleSections(in: CGRect) that returns an optional array of integers representing which sections overlap the given bounds rectangle. I won't detail this here as yours will be different. I'm also caching the content size of the collection view as _contentSize since this only changes when a full layout occurs.
With a small number of sections you could probably just invalidate all of them. Be that as it may, we now need to tell the layout how to set up its invalidation context when the bounds changes.
Note: make sure you're calling super to get the context rather than just creating one yourself; this is the proper way to do things.
override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
let context = super.invalidationContext(forBoundsChange: newBounds) as! CustomInvalidationContext
// If we can't determine visible sections or the width has changed,
// we need to do a full layout - just return the default.
guard newBounds.width == _contentSize.width,
let visibleSections = getVisibleSections(in: newBounds)
else { return context }
// Determine which headers need a frame change.
context.justHeaders = true
let sectionIndices = visibleSections.map { IndexPath(item: 0, section: $0) }
context.invalidateSupplementaryElements(ofKind: "Header", at: sectionIndices)
return context
}
Note that I'm assuming your supplementary view kind is "Header"; change that if you need to. Now, provided that you've properly implemented layoutAttributesForSupplementaryView to return a suitable frame, your headers (and only your headers) should update as you scroll vertically.
Keep in mind that prepare() will NOT be called unless you do a full invalidation, so if you need to do any recalculations, override invalidateLayout(with:) as well, calling super at some point. Personally I do the calculations for shifting the header frames in layoutAttributesForSupplementaryView as it's simpler and just as performant.
Oh, and one last small tip: on the layout attributes for your headers, don't forget to set zIndex to a higher value than the one in your cells so that they definitely appear in front. The default is 0, I use 1 for my headers.
What I suggest is to create a separate subclass of a UICollectionFlowView
and set it up respectivel look at this example:
import UIKit
class StickyHeadersCollectionViewFlowLayout: UICollectionViewFlowLayout {
// MARK: - Collection View Flow Layout Methods
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let layoutAttributes = super.layoutAttributesForElements(in: rect) else { return nil }
// Helpers
let sectionsToAdd = NSMutableIndexSet()
var newLayoutAttributes = [UICollectionViewLayoutAttributes]()
for layoutAttributesSet in layoutAttributes {
if layoutAttributesSet.representedElementCategory == .cell {
// Add Layout Attributes
newLayoutAttributes.append(layoutAttributesSet)
// Update Sections to Add
sectionsToAdd.add(layoutAttributesSet.indexPath.section)
} else if layoutAttributesSet.representedElementCategory == .supplementaryView {
// Update Sections to Add
sectionsToAdd.add(layoutAttributesSet.indexPath.section)
}
}
for section in sectionsToAdd {
let indexPath = IndexPath(item: 0, section: section)
if let sectionAttributes = self.layoutAttributesForSupplementaryView(ofKind: UICollectionElementKindSectionHeader, at: indexPath) {
newLayoutAttributes.append(sectionAttributes)
}
}
return newLayoutAttributes
}
override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard let layoutAttributes = super.layoutAttributesForSupplementaryView(ofKind: elementKind, at: indexPath) else { return nil }
guard let boundaries = boundaries(forSection: indexPath.section) else { return layoutAttributes }
guard let collectionView = collectionView else { return layoutAttributes }
// Helpers
let contentOffsetY = collectionView.contentOffset.y
var frameForSupplementaryView = layoutAttributes.frame
let minimum = boundaries.minimum - frameForSupplementaryView.height
let maximum = boundaries.maximum - frameForSupplementaryView.height
if contentOffsetY < minimum {
frameForSupplementaryView.origin.y = minimum
} else if contentOffsetY > maximum {
frameForSupplementaryView.origin.y = maximum
} else {
frameForSupplementaryView.origin.y = contentOffsetY
}
layoutAttributes.frame = frameForSupplementaryView
return layoutAttributes
}
// MARK: - Helper Methods
func boundaries(forSection section: Int) -> (minimum: CGFloat, maximum: CGFloat)? {
// Helpers
var result = (minimum: CGFloat(0.0), maximum: CGFloat(0.0))
// Exit Early
guard let collectionView = collectionView else { return result }
// Fetch Number of Items for Section
let numberOfItems = collectionView.numberOfItems(inSection: section)
// Exit Early
guard numberOfItems > 0 else { return result }
if let firstItem = layoutAttributesForItem(at: IndexPath(item: 0, section: section)),
let lastItem = layoutAttributesForItem(at: IndexPath(item: (numberOfItems - 1), section: section)) {
result.minimum = firstItem.frame.minY
result.maximum = lastItem.frame.maxY
// Take Header Size Into Account
result.minimum -= headerReferenceSize.height
result.maximum -= headerReferenceSize.height
// Take Section Inset Into Account
result.minimum -= sectionInset.top
result.maximum += (sectionInset.top + sectionInset.bottom)
}
return result
}
}
then add your collection view to your view controller and this way you will implement the invalidation methods which currently are not getting triggered.
source here
Do reloadLoad cells in performBatchUpdates(_:) make it seems glitchy.
Just pass nil like below to update your cell's height.
collectionView.performBatchUpdates(nil, completion: nil)
EDIT:
I have recently found that performBatchUpdates(_:) only shift the header along with cell new height returned from the sizeForItemAt function. If using collection view cell sizing, your supplementary view may overlaps the cells. Then collectionViewLayout.invalidateLayout will fix without showing the animation.
If you want to go with sizing animation after calling performBatchUpdates(_:), try to calculate (then cache) and return cell's size in sizeForItemAt. It works for me.

Checking if view is added on didSelectItem - Swift

I'm hoping you can help me with my code. I'm trying to make it so that when a cell is clicked it checks if the view has already been added. If it has then it dismisses the view and re-adds it. If it doesn't then it simply adds the view.
Here's my current code:
func collectionView(_ collectionView: UICollectionView,
didSelectItemAt indexPath: IndexPath) {
let vc = NoteViewController()
if self.view.subviews.contains(vc.view) {
print("view removed")
}
else {
let cell = collectionView.cellForItem(at: indexPath) as! AnnotatedPhotoCell
sourceCell = cell
vc.picture = resizeImage(image: cell.imageView.image!,
targetSize: CGSize(width:(view.bounds.width - 45),height: 0))
vc.comment = cell.commentLabel
vc.parentVC = parentVC.self
parentVC.self.addChildViewController(vc)
parentVC.self.view.addSubview(vc.view)
vc.didMove(toParentViewController: self)
// 3- Adjust bottomSheet frame and initial position.
let height = view.frame.height
let width = view.frame.width
vc.view.frame = CGRect(x:0, y:self.view.frame.maxY,
width: width, height: height)
}
}
Hoping you can help me with this.

Resources