Uicollectionview Flowlayout Not Wrapping Cells Correctly

UICollectionView flowLayout not wrapping cells correctly

There is a bug in UICollectionViewFlowLayout's implementation of layoutAttributesForElementsInRect that causes it to return TWO attribute objects for a single cell in certain cases involving section insets. One of the returned attribute objects is invalid (outside the bounds of the collection view) and the other is valid. Below is a subclass of UICollectionViewFlowLayout that fixes the problem by excluding cells outside of the collection view's bounds.

// NDCollectionViewFlowLayout.h
@interface NDCollectionViewFlowLayout : UICollectionViewFlowLayout
@end

// NDCollectionViewFlowLayout.m
#import "NDCollectionViewFlowLayout.h"
@implementation NDCollectionViewFlowLayout
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *attributes = [super layoutAttributesForElementsInRect:rect];
NSMutableArray *newAttributes = [NSMutableArray arrayWithCapacity:attributes.count];
for (UICollectionViewLayoutAttributes *attribute in attributes) {
if ((attribute.frame.origin.x + attribute.frame.size.width <= self.collectionViewContentSize.width) &&
(attribute.frame.origin.y + attribute.frame.size.height <= self.collectionViewContentSize.height)) {
[newAttributes addObject:attribute];
}
}
return newAttributes;
}
@end

See this.

Other answers suggest returning YES from shouldInvalidateLayoutForBoundsChange, but this causes unnecessary recomputations and doesn't even completely solve the problem.

My solution completely solves the bug and shouldn't cause any problems when Apple fixes the root cause.

CollectionView FlowLayout custom cell rendering issues

Thanks to some help from @nemecek_filip at the HWS forums, I got it solved!
Different approach to the gradient using a custom gradient view!

Hereby the changed and working code:

collectionView cell:

//
// NormalProjectCell.swift
//

import UIKit

class NormalProjectCell: UICollectionViewCell, SelfConfiguringProjectCell {
//MARK: - Properties
let titleLabel = ProjectTitleLabel(withTextAlignment: .center, andFont: UIFont.preferredFont(forTextStyle: .title3), andColor: .label)
let lastEditedLabel = ProjectTitleLabel(withTextAlignment: .center, andFont: UIFont.preferredFont(forTextStyle: .caption1), andColor: .secondaryLabel)
let imageView = ProjectImageView(frame: .zero)
var stackView = UIStackView()
var backgroundMaskedView = GradientView()

//MARK: - Init
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.cornerRadius = 35

let seperator = Separator(frame: .zero)

stackView = UIStackView(arrangedSubviews: [seperator, titleLabel, lastEditedLabel, imageView])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.distribution = .fillProportionally
stackView.spacing = 5
stackView.setCustomSpacing(10, after: lastEditedLabel)
stackView.insertSubview(backgroundMaskedView, at: 0)
contentView.addSubview(stackView)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

//MARK: - Layout
override func layoutSubviews() {
super.layoutSubviews()

NSLayoutConstraint.activate([
titleLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 20),

stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
stackView.topAnchor.constraint(equalTo: contentView.topAnchor),
stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])

backgroundMaskedView.translatesAutoresizingMaskIntoConstraints = false
backgroundMaskedView.pinToEdges(of: stackView)
}

//MARK: - Configure
func configure(with project: ProjectsController.Project) {
titleLabel.text = project.title
lastEditedLabel.text = project.lastEdited.customMediumToString

imageView.image = Bundle.getProjectImage(project: project)
}
}

And the GradientView:

//
// GradientView.swift
//

import UIKit

class GradientView: UIView {
var topColor: UIColor = UIColor.tertiarySystemBackground
var bottomColor: UIColor = UIColor.systemPurple

override class var layerClass: AnyClass {
return CAGradientLayer.self
}

override func layoutSubviews() {
(layer as! CAGradientLayer).colors = [topColor.cgColor, bottomColor.cgColor]
(layer as! CAGradientLayer).locations = [0.0, 0.40]
}
}

UICollectionView does not return the cells correctly

Maybe...

1. NSDate *date = [calendar dateFromComponents:components];
2. NSDateComponents *compWeek = [calendar components:NSCalendarUnitWeekday fromDate:date];
3.
4. NSInteger weekDay = [compWeek weekday];
5.
6. /******* Numero di giorni del mese in corso *******/
7. /*************************************************/
8. NSInteger numberOfDay = [KPSmallCalendarDayData dayOfMonthFromDate:[NSDate date]];

It looks like you want to use date from line 1, but you are passing a new date object in line 8 ([NSDate date] which == today). Should line 8 be:

NSInteger numberOfDay = [KPSmallCalendarDayData dayOfMonthFromDate:date];

In iOS 12, when does the UICollectionView layout cells, use autolayout in nib

For all solutions, note that there is no need to explicitly call reloadData in viewDidLoad: it will happen automatically.

Solution 1

Inspired by Samantha idea: invalidateLayout asynchronously in viewDidLoad.

override func viewDidLoad() {
super.viewDidLoad()

//[...]

for _ in 0 ..< 1000 {
array.append(randomKeyByBitLength(Int(arc4random_uniform(8)))!)
}

DispatchQueue.main.async {
self.collectionView.collectionViewLayout.invalidateLayout()
}
}

Solution 2

(imperfect, see DHennessy13 improvement on it)

Based on Peter Lapisu answer. invalidateLayout in viewWillLayoutSubviews.

override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
collectionView.collectionViewLayout.invalidateLayout()
}

As noted by DHennessy13, this current solution with viewWillLayoutSubviews is imperfect as it will invalidateLayout when rotating the screen.

You may follow DHennessy13 improvement regarding this solution.

Solution 3

Based on a combination of Tyler Sheaffer answer, Shawn Aukstak port to Swift and Samantha idea. Subclass your CollectionView to perform invalidateLayout on layoutSubviews.

class AutoLayoutCollectionView: UICollectionView {

private var shouldInvalidateLayout = false

override func layoutSubviews() {
super.layoutSubviews()
if shouldInvalidateLayout {
collectionViewLayout.invalidateLayout()
shouldInvalidateLayout = false
}
}

override func reloadData() {
shouldInvalidateLayout = true
super.reloadData()
}
}

This solution is elegant as it doesn't require to change your ViewController code. I've implemented it on branch AutoLayoutCollectionView of this sample project https://github.com/Coeur/StackOverflow51375566/tree/AutoLayoutCollectionView.

Solution 4

Rewrite UICollectionViewCell default constraints. See Larry answer.

Solution 5

Implement collectionView(_:layout:sizeForItemAt:) and return cell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize). See matt answer.

Flowlayout left alignement in collection view

Fixed it with the TagCellLayout pod : https://github.com/riteshhgupta/TagCellLayout

UICollectionViewCells with UICollectionViewFlowLayout - strange layout happening

My answer is very specific and I am not sure it will help anyone.

The problem was that I had a constraint on the bottom of the grey view. After I changed this constraint to a less than or equal too then for some reason it worked.

Now I know this does not explain why it was not happening to every cell but it fixed my problem.

As such Harsh's answer might also be worth looking at if you have landed here after doing a search.

Edit: there also appears to be some bugs in the 6.0 UiCollectionView controller which seem to be fixed in 6.1



Related Topics



Leave a reply



Submit