optimize performance of UICollectionView - ios

I need to dynamically calculate the height of cell of a UICollectionView. I am using this function
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
The problem is that it returns the size of every cell before it is displayed. If there are 120 items in the array it will calculate 120 cell sizes before the
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
is called. This is creating big performance problems. It takes up 10 seconds to load the entire collection. If I dont use sizeforitematindexpath, the collection loads in 1 second. How can I solve this problem ?
I am using Xcode 8.3.3 & Swift 3.0
Here is my exact code for the first delegate
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let json = JSON(mySources[indexPath.row])
var url = NSURL(string: json["cover"]["source"].stringValue)
var data = NSData(contentsOf: url as! URL)
var photo = UIImage(data: data as! Data)
var height1 = photo?.size.height
var boundingRect = CGRect(x:0, y:0, width: 372, height: CGFloat(MAXFLOAT))
let rect = AVMakeRect(aspectRatio: (photo?.size)!, insideRect: boundingRect)
let imageHeight = rect.height
let font = UIFont(name: "Raleway-SemiBold", size: 17)
let titleHeight = heightForLabel(text: json["name"].stringValue, font: font!, width: 160)
let restaurantHeight = heightForLabel(text: json["place"]["name"].stringValue, font: font!, width: 160)
print(restaurantHeight + titleHeight + imageHeight)
return CGSize(width: 372, height:imageHeight + titleHeight + restaurantHeight + 100)
}

UICollectionViewFlowLayout always computes the size of all your cells at once before starting preparing the cells themselves.
However if I remember well you can prevent that by setting a non-zero value to its estimatedItemSize property (the documentation is not crystal clear about it):
The estimated size of cells in the collection view.
Providing an estimated cell size can improve the performance of the collection view when the cells adjust their size dynamically. Specifying an estimate value lets the collection view defer some of the calculations needed to determine the actual size of its content. Specifically, cells that are not onscreen are assumed to be the estimated height.
The default value of this property is CGSizeZero. Setting it to any other value causes the collection view to query each cell for its actual size using the cell’s preferredLayoutAttributesFitting(_:) method. If all of your cells are the same height, use the itemSize property, instead of this property, to specify the cell size instead.
So calling the next piece of code might resolve your performance issues:
(collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.estimatedItemSize = CGSize(width: 375.0, height: 44.0)

You should use reusable cells so that your collection view doesn't load all data at once but only what's on the screen. Which is much less than the total. This will save performance, memory, and will lessen the CPU usage (smoother graphics, less battery consumption).
How to:
for your collectionView:
menuCollectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "MenuCell")
in the function cellForItemAtIndexPath:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MenuCell", for: indexPath as IndexPath)
return cell
}
Hope this helps!

Related

UICollectionViewCell With 2 column grid and dynamic height

I'm trying to have collection view with 2 column but dynamic height.
I have used Autolayout and given required constraints to the Cell
By this way I can calculate the dynamic height but its column grids fails.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! MenuListCollectionViewCell
cell.frame.size.width = collectionView.frame.width/2
cell.menuList = self.menuList[indexPath.row]
let resizing = cell.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize, withHorizontalFittingPriority: UILayoutPriority.required, verticalFittingPriority: UILayoutPriority.fittingSizeLevel)
return resizing
}
This is how I want it to look
Not sure what you mean by "its column grid fails".
Anyway, you need to write a custom collection view layout. The default one (UICollectionViewFlowLayout) allows you to change height of the cells by providing the sizeForItemAt, but that won't change the behavior of the layout that will always arrange cells in rows of the same height (the height of the highest cell).
If I understood correctly, you just want the same layout of this raywenderlich tutorial.
Basically:
Create a subclass of UICollectionViewLayout implementing it's methods:
collectionViewContentSize: return width and height of the collection view content
prepare: where you can calculate the sizes of cells and
collectionView content
layoutAttributesForElements: where you
return an array of UICollectionViewLayoutAttributes in the given
rect
layoutAttributesForItem: where you return the same kind
of attributes, this time specific for an item
assign an object of this class to the collection view layout property
you can use this
This code is somehow written that you can change section inset or minimumInteritemSpacing and this calculate and resize with this parameters
you can use this code or download project from Github
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let numberofItem: CGFloat = 2
let flowLayout = collectionViewLayout as! UICollectionViewFlowLayout
let collectionViewWidth = self.collectionView.bounds.width
let extraSpace = (numberofItem - 1) * flowLayout.minimumInteritemSpacing
let inset = flowLayout.sectionInset.right + flowLayout.sectionInset.left
let width = Int((collectionViewWidth - extraSpace - inset) / numberofItem)
return CGSize(width: width, height: width)
}
I was able to achieve your desired outcome by doing the following.
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 20
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: (self.view.frame.size.width/2 - 5), height: 100)
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = customCollectionView.dequeueReusableCell(withReuseIdentifier: "test", for: indexPath)
cell.layer.backgroundColor = UIColor.red.cgColor
return cell
}
Make sure you have implemented all the correct delegates
UICollectionViewDelegate
UICollectionViewDatasource
UICollectionViewDelegateFlowLayout
where you see height: make it your own desired height as you have specified in your question.
KEY: Make sure in your storyboard you have set the estimate size of the collection view to NONE -> otherwise the code will not work as expected

Determine UICollectionView cell height only by autolayout

I need a collection view which displays cells in a grid. So a standard flow layout is fine for me. However, I want to tell how many cells to show per row, while the cell height should be determined by the autolayout constraints that I put on the cell. Here is my cell layout:
It is quite simple - an image view and two labels below it. Now the image view has an aspect ratio constraint (1:1) which means whenever the width is known for the cell the height should automatically be known by the auto layout rules (there are vertical constraints going through: celltop-image-label1-label2-cellbottom).
Now, since I don't know any other good way to tell the collection view to show 2 items per row, I have overridden UICollectionViewDelegateFlowLayout methods:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let availableWidth = collectionView.frame.width - padding
let widthPerItem = availableWidth / itemsPerRow
return CGSize(width: widthPerItem, height: widthPerItem)
}
As you can see, since I don't know the item height I return the same thing as the width, hoping that the autolayout will fix it later. I also set the estimatedItemSize in order the whole mechanism to start working.
The results are quite strange - it seems like the collection view doesn't event take into account the width I return there, mostly depending on the label lengths:
I have seen some other answers where people recommend manually calculating the cell size for width, like telling "layout yourself, then measure yourself, then give me your size for this width", and even though it would still run the autolayout rules under the hood, I would like to know if there is a way of doing this without manually messing with the sizes.
You can easily find out the height of your collectionView cell in the storyboard's Size inspector, as shown below:
Now, just pick up this height from here, and pass it to the overridden UICollectionViewDelegateFlowLayout method:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let availableWidth = collectionView.frame.width - padding
let widthPerItem = availableWidth / itemsPerRow
return CGSize(width: widthPerItem, height: **114**)
}
And you will get the desired output.
I ended up implementing a trick to move everything to autolayout. I completely removed the delegate method func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize and added a width constraint for my cell content in the interface builder (set the initial value to something, that's not important). Then, I created an outlet for that constraint in the custom cell class:
class MyCell: UICollectionViewCell {
#IBOutlet weak var cellContentWidth: NSLayoutConstraint!
func updateCellWidth(width: CGFloat) {
cellContentWidth.constant = width
}
}
Later, when the cell is created, I update the width constraint to the precalculated value according to the number of cells that I want per row:
private var cellWidth: CGFloat {
let paddingSpace = itemSpacing * (itemsPerRow - 1) + sectionInsets.left + sectionInsets.right
let availableWidth = collectionView.frame.width - paddingSpace
return availableWidth / itemsPerRow
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MyCell", for: indexPath) as! MyCell
...
cell.updateCellWidth(width: cellWidth)
return cell
}
And this, together with autolayout cell sizing enabled, will lay out the cells correctly.

Dynamic cell height in UICollectionView

I'm having a custom UICollectionViewCell which contains many objects but overall, it needs to connect to the database, grab stored image urls and populate a UIImageView by appending each photo (the number of photos is optional - from 1 to 10) to a UIStackView.
Now, I get everything except that the height of the cell is hard coded, so, it's not working. I'm not sure how to get the size though. Here's my code.
This is where the height is hardcoded.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
let width = collectionView.bounds.width
let size = CGSize(width: width, height: 800)
return size
}
This is where the UIImageView is being setup. photoURLs is the array of the downloaded URLs from the database.
if let photoURLString = post?.photoURLs
{
for urlString in photoURLString
{
let photoURL = URL(string: urlString)
let imageView = UIImageView()
imageView.sd_setImage(with: photoURL)
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.widthAnchor.constraint(equalTo: postImagesStackView.widthAnchor)
postImagesStackView.addArrangedSubview(imageView)
postImagesStackView.layoutIfNeeded()
}
}
Also, how to leave a little bit of space between each photo? Like 1 point?
I think that you need to calculate cell height.
In this case, sum images height before you call reload collectionView, or fix imageView height and multiplies images count with height.
Another solution is make cell for each image.
Instead of stackview, you could using section and custom collectionview layout.
Like woosiki indicated, you could try just calculating a value based on image sizes returned. Once the you get the response and compute the full height, then call collectionView.reloadData() and reference that fullHeight property for each backing object. Something like below, though make it safer with if let/where and default/max height.
func collectionView(_ collectionView: UICollectionView, layout
collectionViewLayout: UICollectionViewLayout, sizeForItemAt
indexPath: IndexPath) -> CGSize
{
let imageObject = dataSource.items[indexPath.row]
let width = collectionView.bounds.width
let size = CGSize(width: width, height: imageObject.fullHeight)
return size
}
For a border/spacing, could include slightly larger parent view if you end up including captions or metadata anyway or just try documentation example:
https://developer.apple.com/documentation/uikit/nslayoutanchor

Calculating collectionView cell height dynamically

Im currently attempting to calculate the height of a UICollectionViewCell dynamically but using a UILabel (the dynamic property) and the current height of the cell. The height is calculated by getting height for sizeThatFits of the label within the cell and adding that value to the original height of the cell. For some reason this always returns 0 in sizeForItemAt. I'm pretty sure the problem is the size is being calculated before the cell is created thus will always return 0. Is there anyway around this? (The UI for the cell is created within the class itself, not at celllForItemAt could that also be a problem?)
var descHeight: CGFloat = 0
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "desc", for: indexPath) as! ImageDescCell
descHeight = cell.descriptionLbl.sizeThatFits(cell.frame.size).height + cell.bounds.height
return cell
}
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let w = collectionView.bounds.width
return CGSize(width: w, height: descHeight)
}
NSAttributedString can tell you how big an instance is, given a bound. So assuming you know how wide the label is and you know what the text is and what its attributes are you can get the size without ever creating a label:
let width = CGFloat(200)
let attributedString = NSAttributedString(string: "Test", attributes: [NSFontAttributeName : UIFont(name: "Helvetica", size: 18)!])
let boundingRect = attributedString.boundingRect(with: CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
swift-3.0
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
{
let width = self.collectionView.frame.size.width
let Height = self.view.frame.size.height - 64 // 64 is a navigation bar height minus.
let cellSize:CGSize = CGSize(width: width, height: Height )
return cellSize
}

Resize images automatically in CollectionViewCell in CollectionView with Auto Layout

I have a collectionView with horizontal UICollectionViewFlowLayout.
I am trying to achieve:
If a device orientation is portrait, UIImageView width will be qual to view.width and let the height be calculated automatically (like it usually happens with Auto Layout). And the same for the landscape mode. Example - standard photo app on the Iphone.
Unfortunately i don't see how to achieve it with autoLayout. I set constraints on UIImageView for it to be equal in size to the cell. But looks like the sell itself cannot be pinned to the Parent View.
After reading similar questions looks like cells must be resized programmatically using
func collectionView(collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
return CGSize(width: width, height: height)
}
And here i am stuck because i know the width of the screen but don't know how to calculate the height dynamically.
About image height:
I have my image declared like this:
var pageImages = [UIImage]()
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell: ImageDetailViewCell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! ImageDetailViewCell
let curr = indexPath.row
let imgName = data[albumNumber][curr]["image"]
cell.DetailImageView.image = UIImage(named: imgName!)
return cell
}
If you use the proper UIImageView resize setting (say, aspect fit/fill), then you just need to set the cell's height to your collectionView's (you get a pointer to it as one of the ...sizeForItemAtIndexPath... method parameters) height. You also should call the - layoutIfNeeded method on your cell afterwards.
You can use sizeForItemAtIndexPath: to change the size of collection view cell.
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
var numberOfCellInRow : Int = 3
var padding : Int = 5
var collectionCellWidth : CGFloat = (self.view.frame.size.width/CGFloat(numberOfCellInRow)) - CGFloat(padding)
return CGSize(width: collectionCellWidth , height: collectionCellWidth)
}
You can get the size of cell via :
((UICollectionViewFlowLayout) self.collectionViewName).itemSize.height)
You can get the image size via :
let sizeOfImage = image.size
let height = image.height
If you want to change the height then change it manually by return CGSize(width: collectionCellWidth , height: cellheight)

Resources