iOS UICollectionView with self sizing items bug? - ios

I'm trying to implement UICollectionView (flowLayout) with self sizing items.
Implementation is very simple - I have just set estimatedItemSize and set UICollectionViewCell constraints to manage it's size.
Everything works fine at first data reload after collectionView was created, but on another or some other reload few items at the top becomes same size as estimatedItemSize is. If scroll down and up - items size looks good again.
I have spend 2 days with this issue experimenting different cell constraints, trying to setNeedsLayout in various places and other stuff around collectionView. Is it bug?

In this post I had an interesting problem that I found my own answer to. There are two important functions to be using properly for the sizing purposes which are:
minimumInteritemSpacingForSectionAtIndex
insetForSectionAtIndex
sizeForItemAtIndexPath
Example of me using them in Swift
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionView, minimumInteritemSpacingForSectionAtIndex section: Int) -> CGFloat {
return 0
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAtIndex section: Int) -> UIEdgeInsets {
return UIEdgeInsetsMake(0, 0, 0, 0)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: IndexPath) -> CGSize {
let screenRect: CGRect = collectionView.bounds
let screenWidth: CGFloat = screenRect.size.width
let cellWidth: CGFloat = screenWidth / 24
//Replace the divisor with the column count requirement. Make sure to have it in float.
let size: CGSize = CGSize(width: cellWidth, height: cellWidth)
return size
}
The biggest secret is to be careful about attaching your views to the trailing edge and the bottom layout. If you only attach them to leading and the top, then you can set the width and height programmatically through frame or constraints. I think that doing it through constraints is a little more straight forward, though in my personal project I have chose to do it the other way because it makes the functionality slightly cleaner in my opinion.
Try to accomplish as much of a cells layout as possible inside of cellForItemAt.

Related

Calculating cell width in UICollectionView inside ContainerView

I have a UICollectionView and I have implemented the delegate to calculate the width of my cells for 0 spacing between cells.
It works great on its own, but when I have it inside a container view less than the size of the device, iOS incorrectly works out the spacing between the cells adding a horizontal space I don't want.
I have verified the width I am using to calculate the cell size is correct, so I'm not sure what is causing the problem.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let widthPerItem = view.frame.width / itemsPerRow
return CGSize(width: widthPerItem, height: 60)
}
You are calculating your cell size based upon the size of the view. Since the collectionView doesn't take up the full width of the screen, you are getting the incorrect value. Instead, you should base your calculation on the width of the collectionView itself.
Use the bounds of the collectionView instead of the frame of the view:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let widthPerItem = collectionView.bounds.width / itemsPerRow
return CGSize(width: widthPerItem, height: 60)
}
Ok, the problem was actually related to my uicollectionviewcontroller not correctly sizing to it's parent container view. Changing to a uiviewcontroller with an embedded uicollectionview plus constraints fixed the problem.

Changing frames in layoutAttributesForElements slow down animation

I have a custom CollectionViewFlowLayout in which I override layoutAttributesForElements(in rect: CGRect) in order to get the elements positioned like in the design specs.
Everything is fine, but in some cases, to make a "selection mode" animation, I need to update the y position of the frames, but when I do that, my animation suffers a small delay.
If I maintain the y position and update the x, the width and the height of the attribute, the animation is ok.
Why does that happen? How could I avoid it?
Here's a mockup of my layout:
And here's the selected mode:
Basically my animation consists in changing the transform for the visible cells, in a way to make them smaller but centered, and to maintain the alignments and correct spacing I manipulate the frames in layoutAttributesForElements and invalidate the layout.
edit:
other solutions like changing the cellSize and minimumInteritemSpacing doesn't work for me because UICollectionView animation for cellSize change is extremely wonky
As I mentioned up in the comments, there seems to be an easier approach: I was able to get a pretty smooth implementation by just altering the values returned from collectionView(_:layout:sizeForItemAt:), collectionView(_:layout:minimumLineSpacingForSectionAt:) and collectionView(_:minimumInteritemSpacingForSectionAt:) as appropriate, and calling invalidateLayout() and layoutIfNeeded() inside an animation block at the right moment.
The return values for a really basic layout of items might look like this (where separatorSize(isInSelectionMode:) returns larger or smaller values depending on the selection state):
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let totalRowHeight: CGFloat = 100
let widthRatio: CGFloat
switch indexPath.item % 4 {
case 0, 3: widthRatio = (2.0 / 3.0)
case 1, 2: widthRatio = (1.0 / 3.0)
default: widthRatio = 1.0
}
return CGSize(width: (collectionView.bounds.width - separatorSize(isInSelectionMode: selectionMode).width) * widthRatio,
height: totalRowHeight - separatorSize(isInSelectionMode: selectionMode).height)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return separatorSize(isInSelectionMode: selectionMode).height
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return separatorSize(isInSelectionMode: selectionMode).width
}
Then you just toggle selectionMode inside an animation block while invalidating the layout.
I made a demo project here and a demo video here.

How can I set custom horizontal scroll for Collection View in Swift, XCode?

I want to set custom horizontal scroll for my CollectionView, that it will be scrolling by 1 cell (not by the whole width of my screen).
I could set HORIZONTAL scroll, but not custom. (See screens).
1 screen: my extension of my collectionView for UIScrollViewDelegate.
*I saw, that in console (see too) "x" - my offset = 290 - it's true! But on fact it is not 290. Paging was marked in "true" 2 screen: delegate and dataSource.
Help, please!
First you need to set your collectionView scroll direction horizontal (UICollectionViewScrollDirectionHorizontal) and set pagingEnabled to YES
It means your collection view will be scroll horizontally with one by one cell.
Set horizontal direction (Select collection view from XIB or Storyboard)
And for set pagination enable
myCollectionView.pagingEnabled = true
Updated :
You should use of UICollectionViewDelegateFlowLayout delegate
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
let width = UIScreen.main.bounds.size.width
var height = 101.7 // set height as you want
return CGSize(width: width, height: height)
}
If using storyBoard the go to collection view and select the right handed tab "show the size inspector" and change
minimum spacing for cells = 0
minimum spacing for lines = 0
if you want to change directly from code then implement these UICollectionViewDelegateFlowLayout methods
func collectionView(_ collectionView: UICollectionView, layout
collectionViewLayout: UICollectionViewLayout,
minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
must be remember scrolling direction set horizontal
and pagination enabled
and implement this delegate methods into code
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout
collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath:
IndexPath) -> CGSize {
return CGSize(width: collectionView.frame.size.width, height:
collectionView.frame.size.height)
}
}
You could create a custom UICollectionViewFlowLayout that will center the cells in the screen and then page one at a time. Karmadust have written a good blog post that I have successfully used in the past to do the same thing
http://blog.karmadust.com/centered-paging-with-preview-cells-on-uicollectionview/

Adaptive UICollectionView Cell Width

I am new to collection view and Auto Layout and I have a problem making the cell sizes adapt to the various devices in the simulator. I am using the flow-layout and I set the sizes in the size inspector. The image I've provided shows the way I need the cells to look(canvas on the left iPhone5)on all devices. The iPhone4 display is good but the 6s is incorrect.
Can someone please show me how this is done as I cannot find the precise information I am looking for.
Thanks
Also, I'm not sure why the iPhone5 doesn't display the cells on the preview section like the 4&6 do..? any clues..?
screen shot1
With UICollectionView, you need to calculate the size of your cells, and the space around them (insets and interitem spacing) using delegate methods.
Something like this should work for you:
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAtIndex section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 8, left: 4, bottom: 0, right: 4)
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAtIndex section: Int) -> CGFloat {
return CGFloat(8)
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
let cellWidth = (view.bounds.size.width - 16) / 2
let cellHeight = cellWidth * 0.8 // multiply by some factor to make cell rectangular not square
return CGSize(width: cellWidth, height: cellHeight)
}
Within a UICollectionViewCell is where you would use Auto Layout to position the things in the cell, like labels, images, etc.

Placing the cells in a collection view in the centre rather than the outsides in swift

By default, a collection view will attempt to space out the cells in the collection by spacing them evenly so that there will be one cell touching either side of the collection view. Example:
But, when the cell is too wide to fit multiple cells in one row on the collection view, it will centre the cell in the collection view.
Is there a way to make the behaviour of centring cells, like in the 3rd picture, the default for the collection view so that there is 0 space between the individual cells and space on either side to the edges of the collection view?
Not that I'm aware of. You should implement something like this:
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
CGFloat width = UIScreen.mainScreen().bounds.size.width
return CGSize(width, anyHeight)
}
and then implement the delegate method below as well:
func collectionView(collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
insetForSectionAtIndex section: Int) -> UIEdgeInsets {
return UIEdgeInsetsZero
}
Make sure to implement rotation handling too if it's applicable.
My answer would be to use a carefully set up something like
let insets = UIEdgeInsets(top: yourvalues, left: yourvalues, bottom: yourvalues, right: yourvalues),
and feed it to either YourCollectionView.contentInset, or
func collectionView(collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
insetForSectionAtIndex section: Int) -> UIEdgeInsets {
return sectionInsets
}
This should work! Lemme know if you have any troubles with it!

Resources