Understanding UICollectionViewFlowLayout sizing and spacing - ios

I am working with a UICollectionView using UICollectionViewFlowLayout and have some difficulties to understand item sizing an spacing. I know that there several methods to adapt sizing and spacing (using the delegate methods, overriding FlowLayout, etc.). However without understanding the logic behind these values in the first place, it is quite hard to adapt them properly.
The following results have been created a default UICollectionViewController with a default UICollectionViewCell without any subclasses. Only the following settings haven been made:
Specified the cell size to be 200 x 200 in IB
Placed a Label inside the cell and centered in vertically and horizontal
Code:
private let reuseIdentifier = "Cell"
class MyViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
override func viewDidLoad() {
super.viewDidLoad()
var layout: UICollectionViewFlowLayout {
let layout = UICollectionViewFlowLayout()
layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
layout.minimumLineSpacing = 0
layout.minimumInteritemSpacing = 5
layout.sectionInset = UIEdgeInsets(top: 20, left: 20, bottom: 0, right: 20)
layout.sectionInsetReference = .fromContentInset
return layout
}
collectionView.collectionViewLayout = layout
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 2
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 3
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
return cell
}
/*func collectionView(_ collectionView : UICollectionView,layout collectionViewLayout:UICollectionViewLayout,sizeForItemAt indexPath:IndexPath) -> CGSize {
var width = view.frame.width
return CGSize(width: collectionVw.frame.size.height, height: collectionVw.frame.size.height)
}*/
}
Using different values for layout.minimumInteritemSpacing = 5 creates results I do not understand:
Why are the items 50 x 50px in size? I know that one can use ...sizeForItemAt to specify explicit dimensions. However, setting the size in IB should also work, shouldn't it? Why is the IB size of 200 x 200px ignored? Why 50x50, is this the default size or where is this specified? Solved in the answer by #Larme
I know that minimumInteritemSpacing does not set the explicit spacing but only a minimum. However, how is the value computed?
Why is the a spacing of 7.5px for a value of 5? Why result values 10-25 result all in the same a spacing of 26.5px?
So: How exactly are sizes and spacing computed here?

You have multiple questions (not really recommended), I'll answer what I can do.
Why are the items 50 x 50px in size? I know that one can use ...sizeForItemAt to specify explicit dimensions. However, setting the size in IB should also work, shouldn't it? Why is the IB size of 200 x 200px ignored? Why 50x50, is this the default size or where is this specified?
You are doing:
collectionView.collectionViewLayout = layout
Where layout is newly created.
You aren't using the previous settings in InterfaceBuilder, you are overriding them by code.
And your created for layout, its itemSize hasn't been set. And from the doc, if not set, it's 50x50.
Why are the items aligned to the left for these values and distribued evenly of the complete width for value of 30?
Are you sure about that? Default Layout (meaning, not a inherited from UICollectionViewFlowLayout) will behave like paragraphs styling.
I'll take the horizontal layout (same logic can be applied in vertical, but analogy with text paragraphs would be strange):
If you have multilines text, the first line would take as much width as possible, but the last line will not, keeping it "left" aligned.
For your spacing calculations, according to the doc of minimumInterItemSpacing:
For a vertically scrolling grid, this value represents the minimum spacing between items in the same row. For a horizontally scrolling grid, this value represents the minimum spacing between items in the same column. This spacing is used to compute how many items can fit in a single line, but after the number of items is determined, the actual spacing may possibly be adjusted upward.
But, I'm wondering, what would happen if you override viewDidLayoutSubview(), and call collectionView.collectionViewLayout?.invalidateLayout(); collectionView.collectionViewLayout?.prepareLayout().

Related

Dynamic Cell Height for Custom Collection View Layout

I have a collection view which makes use of a custom layout. I'm trying to calculate the height dynamically, but the problem is sizeForItemAt:indexPath is called before cellForItemAt:indexPath.
My cells get loaded in cellForItemAt. But since sizeForItemAt is called before cellForItemAt, then I can't use my calculated height.
I know with Apple's FlowLayout I can just set the estimatedItemSize for the layout. I'm not sure how to do it with a custom layout.
Please advise. Thank you!
I had the same problem, my app uses custom layout with dynamic height. I found that with custom layout that doesn't extend UICollectionViewFlowLayout or any other default layout, dynamic height will not work because as per Apple documentation (and like you probably noticed) with a completely custom layout you have to predefine all the cells X, Y, width, height before the cell load and before you even have data.
I changed my custom layout to subclass UICollectionViewFlowLayout and implemented UICollectionViewDelegateFlowLayout. When this method is called the cell did not load yet but the cell's data is available since I know what the cell should look like (assuming you are using cell prototypes) I could calculate the cell width and height using its data and index, something like this:
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
// get the cell's data
if let data = self.fetchedResultsController.fetchedObjects![indexPath.row] as? YourDataType {
// carculate the cell width according to cell position
let cellWidth = carculateCellWidth(indexPath.row)
var cellHeight : CGFloat = 0
// assuming the cell have a label, set the label to have the same attributes as set in the storyboard or set programmatically
let label = UILabel()
label.numberOfLines = 0
label.font = UIFont.preferredFont(forTextStyle: .subheadline)
label.text = data.text
// carculate the height of the cell, assuming here the label width equal the cell width minus 10px left and right padding.
cellHeight += label.systemLayoutSizeFitting(CGSize(width:cellWidth-20, height: CGFloat(Float.greatestFiniteMagnitude))).height
return CGSize(width: cellWidth, height: cellHeight)
}
return .zero
}
This is not a very elegant solution but it works.

Swift auto layout in UICollectionView with intrinsic height of cells

I'm having a hard time with auto layout and UICollectionView. I have a vertical UICollectionVIewFlowLayout that contains two different types of cells, as in the image below:
The SingleCard cell contains a subview with an image (UIImageView). The HorizList cell contains a subview which is a horizontal UICollectionView (a list of thumbnail photos). The heights of the two cell types are as follows:
SingleCard: the width of the cell shall be the same as the screen width. The height shall be dynamic to the height of the UIImageView (to keep proportions of the image and have it fill screen width no matter of device size)
HorizList: the height is constant, say 200 points.
My attempt, in the CollectionViewController is to do
override func viewDidLoad() {
...
let layout = collectionView?.collectionViewLayout as! UICollectionViewFlowLayout
layout.estimatedItemSize = CGSize(width: 200.0, height: 235.0)
}
This I do to get the two cell's intrinsic size to apply. Then I do not implement
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
at all.
My question is, what do I implement in
override var intrinsicContentSize: CGSize {
??
}
for the two different cell types? Maybe this is not the only thing I need to do, if so I'm grateful for additional hints.
Thanks!
/ola

Simply using storyboard, constraints to set size of cell in UICollectionView

Here's a UICollectionView, and the cell in purple:
Quite simply, I want the cells to be 1/2 of the collection view width. (So TBC, it will be a two rows arrangement of cells in the collection view.)
(The collection view is simply fullscreen, so each cell is half the screen width.)
How do you do this in storyboard?
If I try to control-drag in the normal way, it basically doesn't work.
These are simple totally static cells (not dynamic).
For anyone googling here, to save your time: Here's exactly (2016) the simplest way to make a two-across UICollectionView layout; no gaps between the cells.
// Two - two-across UICollectionView
// use a completely standard UIViewController on the storyboard,
// likely change scroll direction to vertical.
// name the cell identifier "cellTwo" on the storyboard
import UIKit
class Two:UICollectionViewController
{
override func viewDidLoad()
{
super.viewDidLoad()
let w = collectionView!.bounds.width / 2.0
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
layout.itemSize = CGSize(width:w,height:w)
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
collectionView!.collectionViewLayout = layout
// Note!! DO NOT!!! register if using a storyboard cell!!
// do NOT do this:
// self.collectionView!.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
}
override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int
{ return 1 }
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{ return 5 }
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
{
return collectionView.dequeueReusableCellWithReuseIdentifier("cellTwo", forIndexPath: indexPath)
}
}
You can't do it in the storyboard. The collection view width is not known until runtime, and collection view cells are not under autolayout, so you cannot express the notion "1/2 the width" of anything else. (If you did know the collection view width in advance, you could use the flow layout in the storyboard to set the cell size absolutely, by dividing in your head; but you don't know it, because the width differs depending on the device.)

SWIFT - Collection View Cell size

I have an app that uses photos from web and put them into the collection view.
in collection view i have 3 rows of cells 1:1 size which calculates from screen width / 3.
every thing is working good but there is a thing, on for ex. iphone 6s+ the cells are all tightly get together with no spacings at all. but on iphone 5s i getting some spacing between cells, in only vertical way as on the screenshot.
iPhone 6s+ Screenshot
iPhone 5s Screenshot
there is some code:
let screenSize: CGRect = UIScreen.mainScreen().bounds
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
{
return CGSize(width: screenSize.width / 3, height: screenSize.width / 3)
}
i have also checked if it is an image view problem but it is not.
What can i do to remove those spacings?
Creating CollectionView and then fit cells and spacing programmatically, you can try to add minimumInteritemSpacingForSectionAtIndex and minimumLineSpacingForSectionAtIndex on your own code.
func collectionViewLaunch() {
// layout of collectionView
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
// item size
layout.itemSize = CGSizeMake(self.view.frame.width / 3, self.view.frame.size.width / 3)
// direction of scrolling
layout.scrollDirection = UICollectionViewScrollDirection.Vertical
// define frame of collectionView
let frame = CGRectMake(0, 0,
self.view.frame.size.width, self.view.frame.size.height - self.tabBarController!.tabBar.frame.size.height - self.navigationController!.navigationBar.frame.size.height)
// declare collectionView
collectionView = UICollectionView(frame: frame, collectionViewLayout: layout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.alwaysBounceVertical = true
collectionView.backgroundColor = .whiteColor()
self.view.addSubview(collectionView)
self.collectionView.hidden = false
// define cell for collection view
collectionView.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
// call function to load posts
loadPosts()
}
// cell line spacing
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAtIndex section: Int) -> CGFloat {
return 0
}
// cell inter spacing
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout,
minimumLineSpacingForSectionAtIndex section: Int) -> CGFloat {
return 0
}
// cell numb
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return picArray.count
}
Hope help you.
Do the original sum, round it down and multiply back by the number of columns.
Adjust the frame width of the collectView to this value and then cell widths will always fit the view perfectly.
Since the iPhone 5s' width is 640, dividing it by 3 would result in 213,33333333. Since iOS doesn't like those values, it will correct the value to 213, which will create a spacing when having three of those next to each other, since 213*3 does not equal 640. On the iPhone 6s Plus, the width is 1080 which is equally dividable by 3, resulting in 360, Here, no spacing will occur.
Try finding a divider which equals a natural number for any screen size and the spaces should be gone, or you try to find another solution.
I had a similar issue with horizontal and vertical collection views.
I was using a slider to set the number of columns and then resize the UIImages in the collection accordingly to the new cell size
On top of this I cut an image of an arrow in two and placed each half at the sides of the collection view so that when it displayed it had the result of drawing a full arrow in between images... I couldn't be bothered with custom seperator inserts at the time...which is where the arrow "should" have lived.
Hence every now and then I had a pixel wide gap in the arrows depending on screensize and orientation.
It IS all in the rounding up of the division result.
You have to find a width/height that divides as best you can to fit the screen most used.
You can apply some conditional resizing of the views if the values from the sums are not integers but finding the correct value to replace it with meant having to specify every eventuality.
In the end I gave up and "lived" with the single-pixel-wide gap

How to approach creating Dynamic UICollectionView Item Sizes?

I've been working on this problem for probably 24 hours now and I can't seem to find out how you're intended by Apple to set the dynamic height of a CollectionViewCell.
This is where I'm completely lost:
1.) I need each Cell's frame size in my UICollectionView to be determined by the UILabel's text within each cell. The UILabel's frame is constrained to the edges of the cell, so simply grabbing the UILabel.frame's height gives me the precise value I need.
2.) 1 BIG PROBLEM, the UICollectionView's sizeForItemAtIndexPath's function is executed first before the UILabels are even populated in the UICollectionView's cellForItemAtIndexPath function.
Below is a hack I've tried which failed misrably:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("MsgCell", forIndexPath: indexPath) as! CustomCollectionViewCell
cell.txtLabel.text = dummyChatFeed[indexPath.row].messageBody
cell.txtLabel.sizeToFit()
cell.lineBreakMode = NSLineBreakMode.ByWordWrapping
// store the value of this proper label height back into the array:
dummyChatFeed[indexPath.row].frameHeight = cell.txtLabel.frame.height
return cell
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
// grab that proper label height again:
var theHeightThatFits = dummyChatFeed[indexPath.row].frameHeight
return CGSize(width: Double(self.view.frame.width), height: Double(theHeightThatFits))
}
The dummyChatFeed is an NSObject Class which contains only these 4 properties:
var messageBody = ""
var author = ""
var frameHeight = CGFloat(0)
var indexPathRow = 0
This solution I've tried barely works, and it's buggy as hell since the height of the Cell depends on the label within it. But what is the best way to go from here?
I thought initially that it's best to keep an array of objects with one property of the object that stores the label text while another property of the object should store the value of the recommended height, but I've just learned the hard way that it would never work.
Am I suppose to programmatically declare a UILabel within my dummyChatFeed and calculate the width and height from there based on text and font?
I need my app to be backwards compatible with iOS 8.4 so I can't use UIStackViews.

Resources