Changing frames in layoutAttributesForElements slow down animation - ios

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.

Related

How to know collection view's `referenceSizeForHeaderInSection` method call has finished?

I have a viewController with a UICollectionView, to which I'm setting headers I need their height to be calculated dynamically. My implementation of the collectionView(_:layout:referenceSizeForHeaderInSection:) is similar to this:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
let width = collectionView.frame.size.width
customHeader.layoutIfNeeded()
let height = customHeader.container.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
return CGSize(width: width, height: height)
}
I'd need to know when this call has been completed and the size of the headers have been set, in order to trigger some other actions I need to perform.
I've seen that both viewDidLoad() and viewDidLayoutSubviews() methods of the viewController are called before this other one. Is there any way to achieve what I need?

iOS UICollectionView with self sizing items bug?

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.

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.

Display Instagram photos correctly in a CollectionView on all screen sizes

This one has me perplexed. Ok, here we go.
I'm pulling down IG images, they come in 3 sizes:
150x150, 306x306, 640x640
What I would like to do is display them 3 across, on all iPhone screen sizes (5,5S,6,6+). The 306x306 sizes come down super fast, and look great. So that's my starting point (150x150 are a bit blurry, 640x640 is also an option, I can also use those, just trying to save some bandwidth).
The edges can be flush to the sides, with a 1 px/pt line between them. I have kind of wracked my brain around this. My understanding is that the function (below) should override any other setting in my Storyboard, and where I should be focused (I've printed out the sizes I've captured from println().
My challenge is finding out the final widths and heights needed to work on all screens. I've tried a number of permutations, but have not nailed it yet. I'm close, but no cigar. This is all using a CollectionView in storyboards. Advice appreciated. It's a heck of a problem, spent all day on this one.
I don't think autolayout can help at all, it all has to be done in code, but who knows?
Thanks [lots!] :-)
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
var width = CGRectGetWidth(collectionView.bounds)
println("width: \(width)")
// iphone5 320
// iphone5S 320
// iphone6 375
// iphone 6+ 414
return CGSize(width: NeedCorrectWidthHere, height: NeedCorrectHeightHere)
}
This does partially work, with a gap between images:
var screenWidth = CGRectGetWidth(collectionView.bounds)
var cellWidth = screenWidth/3
return CGSize(width: cellWidth-10, height: cellWidth-10)
This seemed to work. A pretty simple solution in the end.
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
var screenWidth = CGRectGetWidth(collectionView.bounds)
var cellWidth = screenWidth/3
return CGSize(width: cellWidth-2, height: cellWidth-2)
}
func collectionView(collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumLineSpacingForSectionAtIndex section: Int) -> CGFloat {
return 3
}
func collectionView(collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumInteritemSpacingForSectionAtIndex section: Int) -> CGFloat {
return 3
}

Creating Grid Layout using UICollectionView

I'd like to build a collection view layout something similar to grid. With
Row containing sub rows
Columns containing sub columns
Rows with variable height.
Scrolling should be possible in both directions.
It should be possible to add/delete row/columns
I tried using collection flow layout but It started becoming complex. Building a custom layout seems a better option to me.
A few problem faced by me :-
Data structure(Basic class hierarchy) to store grid information(layout only) that supports easy addition/deletion of rows and columns.
Calculating height of content view given that the rows can be of variable size.
Calculating row range that will lie within the visible rect. Right now i to have collect the height information for all the rows and store them in an array and further calculate the content size height. Also to decide which all rows lie within the given rect, I have to apply a predicate. Doing so on every call to "layoutAttributesForElementsInRect" also drops the fps when the no of rows is 500+ still acceptable.
I tried maintaining an NSSet of layout attributes for visible rows and columns and purging/adding attributes as rows/columns move in and out. but this was more slower the creating each and every attributes for visible items. Also What design patterns are best suited for grids?
Last but not the least will it be possible to design something like this with UICollectionView?
Any ideas on how to process with this,
thanks :)
So, I had your same problem and I had solved in this way (swift example).
In this example, cell are drawn squared, 2 columns x N rows.
class CustomViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout
#IBOutlet var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
collectionView.delegate = self;
collectionView.dataSource = self;
}
//MARK: - Collection view flow layout delegate methods -
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
let screenSize = UIScreen.mainScreen().bounds
let screenWidth = screenSize.width
let cellSquareSize: CGFloat = screenWidth / 2.0
return CGSizeMake(cellSquareSize, cellSquareSize);
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAtIndex section: Int) -> UIEdgeInsets {
return UIEdgeInsetsMake(0, 0, 0.0, 0.0)
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAtIndex section: Int) -> CGFloat {
return 0.0
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAtIndex section: Int) -> CGFloat {
return 0.0
}
}

Resources