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
Related
I used auto layout to dynamically calculate the size of the collectionView cell. Some cells are using the dimensions from the reused cells when they first scrolled to view port. As I continue to scroll the collectionView, they will be set to the correct value.
In my sizeForItemAt, I have the following:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if let cachedSize = cachedHeightForIndexPath[indexPath] {
return cachedSize
}
if let cell = collectionView.cellForItem(at: indexPath) {
cell.setNeedsLayout()
cell.layoutIfNeeded()
let size = cell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
cachedHeightForIndexPath[indexPath] = size
print("value is \(size) for indexpath: \(indexPath)")
return size
}
return CGSize(width: ScreenSize.width, height: 0.0)
}
I have a three sessions, with the first section all cell's height theoretically equals to 88, and all the other sections all cell's height equals to 104.
Originally, only the first section is visible. From the console, I can see the height of the cell is set to 88.0 as expected. As I scroll to the remaining sections(the first section will be invisible and the cells will be reused), some cells from second section and third section are using the value 88.0 as the height of the cells when first scrolled to view port instead of 104. As I continue to scroll, the wrong sized cell will be using 104 as the dimension. How do we force all the cells to recalculate the height and don't use the height from old cell.
You have the right idea, but when you measure the cell by its internal constraints by calling systemLayoutSizeFitting, instead of calling systemLayoutSizeFitting on an existing cell (collectionView.cellForItem), you need to arm yourself with a model cell that you configure the same as cellForItem would configure it and measure that.
Here's how I do it (remarkably similar to what you have, with that one difference; also, I store the size in the model):
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let memosize = self.sections[indexPath.section].itemData[indexPath.row].size
if memosize != .zero {
return memosize
}
self.configure(self.modelCell, forIndexPath:indexPath) // in common with cellForItem
var sz = self.modelCell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
sz.width = ceil(sz.width); sz.height = ceil(sz.height)
self.sections[indexPath.section].itemData[indexPath.row].size = sz // memoize
return sz
}
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.
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!
This is my first post, so if there is something that I should do differently, please let me know!
I'm making a photo app, where the I want the photos to fill the UITaleViewCells, and the Cells to expand to fit the images.
I've been struggling to make it work, I've googled everything I can thing of, watched a few youtube videos, but I can't find any that work with UIImageView's. Here is the code I'm working with right now:
override func viewWillAppear(_ animated: Bool) {
tableView.estimatedRowHeight = 100
tableView.rowHeight = UITableViewAutomaticDimension
}
On the UIImageView I have these constraints:
Bottom Margin, Top Margin, Trailing Margin and Leading Margin, when I run my app I get this really weird space at the top and the bottom of my images.
Here is some screenshots:
Well i suggest you to try with UICollectionView for this because for such layouts its better to use Collection View instead Table Views, try this (tempArr means your image array),
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
// create a cell size from the image size, and return the size
let picHeight = tempArr[indexPath.row].valueForKey("imgHeight") as! String
let picWidth = tempArr[indexPath.row].valueForKey("imgWidth") as! String
var picHeightFloat :CGFloat!
var picWidthFloat :CGFloat!
if let n = NSNumberFormatter().numberFromString(picHeight) {
picHeightFloat = CGFloat(n)
}
if let q = NSNumberFormatter().numberFromString(picWidth) {
picWidthFloat = CGFloat(q)
}
let imageSize = CGSize(width: picWidthFloat, height: picHeightFloat)
return imageSize
}
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)