I have a custom UICollectionView layout that resizes when the user scrolls. As the header shrinks at one point it begins to flicker.
I'm guessing the issue is that when the header shrinks the collection view thinks it's out of frame and perhaps dequeues it but then it calculates that it is in frame and re-queues it which might be what's causing the flicker.
class CustomLayout: UICollectionViewFlowLayout, UICollectionViewDelegateFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let layoutAttributes = super.layoutAttributesForElements(in: rect) as! [UICollectionViewLayoutAttributes]
let offset = collectionView!.contentOffset ?? CGPoint.zero
let minY = -sectionInset.top
if (offset.y >= minY) {
let setOffset = fabs(170 - minY)
let extraOffset = fabs(offset.y - minY)
if offset.y <= 170 {
for attributes in layoutAttributes {
if let elementKind = attributes.representedElementKind {
if elementKind == UICollectionElementKindSectionHeader {
var frame = attributes.frame
frame.size.height = max(minY, headerReferenceSize.height - (extraOffset * 1.25))
frame.origin.y = frame.origin.y + (extraOffset * 1.25)
attributes.frame = frame
}
}
}
} else {
for attributes in layoutAttributes {
if let elementKind = attributes.representedElementKind {
if elementKind == UICollectionElementKindSectionHeader {
var frame = attributes.frame
frame.size.height = max(minY, headerReferenceSize.height - (setOffset * 1.25))
frame.origin.y = frame.origin.y + (setOffset * 1.25)
attributes.frame = frame
}
}
}
}
}
return layoutAttributes
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
}
Here is a gif showing the behavior. Notice how it starts out fine and begins to flicker. Also fast scrolling has an undesired effect.
Any suggestions?
I don't think your issue is related to the layout code. I copied and tried using your CustomLayout in a simple sample app with no obvious flickering issues.
Other things to try:
Make sure your collectionView(viewForSupplementaryElementOfKind:at:) function properly reuses the header view, using collectionView.dequeueReusableSupplementaryView(ofKind:withReuseIdentifier:for:)). Creating a new header view each time could cause substantial delays.
Do you have any complex drawing code in the cell itself?
Worst case, you could try profiling the app using Instruments Time Profiler to see what operations are taking up the most CPU cycles (assuming dropped frames are your issue).
Looks like that your collection view moves the header to the back.
Try insert this in code where you're changing frame of the header:
collectionView.bringSubview(toFront: elementKind)
Related
I'm facing a weird issue that I can't seem to figure out or find anything about online.
So I'm trying to replicate the Shazam discover UI with a UICollectionView and a custom UICollectionViewFlowlayout.
So far everything is working pretty well, but when adding the "card stack" effect I (or rather the person who was implementing it) noticed there seems to be a weird issue where on some occasions (or rather, when specific indexes are visible, in the example it's row 5, 9) there will be 4 visible cells instead of 3. My guess would be that this has something to do with cell reuse, but I'm not sure why it's doing this. I looked into the individual cell dimensions and they all seem to be the same so it's not that cells are just sized differently.
Does anyone have an idea as to why this could be happening? Any help or suggestions are really appreciated.
I'll add a code snippet of the custom flowlayout and screenshots below.
You can download the full project here, or alternatively, check out the PR on Github.
Here's a visual comparison:
Source code of the custom flowlayout:
import UIKit
/// Custom `UICollectionViewFlowLayout` that provides the flowlayout information like paging and `CardCell` movements.
internal class VerticalCardSwiperFlowLayout: UICollectionViewFlowLayout {
/// This property sets the amount of scaling for the first item.
internal var firstItemTransform: CGFloat?
/// This property enables paging per card. The default value is true.
internal var isPagingEnabled: Bool = true
/// Stores the height of a CardCell.
internal var cellHeight: CGFloat!
internal override func prepare() {
super.prepare()
assert(collectionView!.numberOfSections == 1, "Number of sections should always be 1.")
assert(collectionView!.isPagingEnabled == false, "Paging on the collectionview itself should never be enabled. To enable cell paging, use the isPagingEnabled property of the VerticalCardSwiperFlowLayout instead.")
}
internal override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let items = NSMutableArray (array: super.layoutAttributesForElements(in: rect)!, copyItems: true)
items.enumerateObjects(using: { (object, index, stop) -> Void in
let attributes = object as! UICollectionViewLayoutAttributes
self.updateCellAttributes(attributes)
})
return items as? [UICollectionViewLayoutAttributes]
}
// We invalidate the layout when a "bounds change" happens, for example when we scale the top cell. This forces a layout update on the flowlayout.
internal override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
// Cell paging
internal override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
// If the property `isPagingEnabled` is set to false, we don't enable paging and thus return the current contentoffset.
guard isPagingEnabled else {
let latestOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
return latestOffset
}
// Page height used for estimating and calculating paging.
let pageHeight = cellHeight + self.minimumLineSpacing
// Make an estimation of the current page position.
let approximatePage = self.collectionView!.contentOffset.y/pageHeight
// Determine the current page based on velocity.
let currentPage = (velocity.y < 0.0) ? floor(approximatePage) : ceil(approximatePage)
// Create custom flickVelocity.
let flickVelocity = velocity.y * 0.4
// Check how many pages the user flicked, if <= 1 then flickedPages should return 0.
let flickedPages = (abs(round(flickVelocity)) <= 1) ? 0 : round(flickVelocity)
// Calculate newVerticalOffset.
let newVerticalOffset = ((currentPage + flickedPages) * pageHeight) - self.collectionView!.contentInset.top
return CGPoint(x: proposedContentOffset.x, y: newVerticalOffset)
}
internal override func finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
// make sure the zIndex of the next card is higher than the one we're swiping away.
let nextIndexPath = IndexPath(row: itemIndexPath.row + 1, section: itemIndexPath.section)
let nextAttr = self.layoutAttributesForItem(at: nextIndexPath)
nextAttr?.zIndex = nextIndexPath.row
// attributes for swiping card away
let attr = self.layoutAttributesForItem(at: itemIndexPath)
return attr
}
/**
Updates the attributes.
Here manipulate the zIndex of the cards here, calculate the positions and do the animations.
- parameter attributes: The attributes we're updating.
*/
fileprivate func updateCellAttributes(_ attributes: UICollectionViewLayoutAttributes) {
let minY = collectionView!.bounds.minY + collectionView!.contentInset.top
let maxY = attributes.frame.origin.y
let finalY = max(minY, maxY)
var origin = attributes.frame.origin
let deltaY = (finalY - origin.y) / attributes.frame.height
let translationScale = CGFloat((attributes.zIndex + 1) * 10)
// create stacked effect (cards visible at bottom
if let itemTransform = firstItemTransform {
let scale = 1 - deltaY * itemTransform
var t = CGAffineTransform.identity
t = t.scaledBy(x: scale, y: 1)
t = t.translatedBy(x: 0, y: (translationScale + deltaY * translationScale))
attributes.transform = t
}
origin.x = (self.collectionView?.frame.width)! / 2 - attributes.frame.width / 2 - (self.collectionView?.contentInset.left)!
origin.y = finalY
attributes.frame = CGRect(origin: origin, size: attributes.frame.size)
attributes.zIndex = attributes.indexPath.row
}
}
edit 1: Just as an extra clarification, the final end result should make it look something like this:
edit 2:
Seems to be happening every 4-5 cards you scroll from my testing.
You have a layout that inherits from flow layout. You overrode layoutAttributesForElements(in rect:) where you take all the elements from super.layoutAttributesForElements and then for each one modify the properties in the method updateCellAttributes.
This is generally a good way to make a subclass of a flow layout. The UICollectionViewFlowLayout is doing most of the hard work - figuring out where each element should be, which elements are in the rect, what their basically attributes are, how they should be padded, etc, and you can just modify a few properties after the "hard" work is done. This works fine when you are adding a rotation or opacity or some other feature that does not change the location of the item.
You get into trouble when you change the items frame with updateCellAttributes. Then you can have a situation when you have a cell that would not have appeared in the frame at all for the regular flow layout, but now SHOULD appear because of your modification. So the attribute is not being returned AT ALL by super.layoutAttributesForElements(in rect: CGRect) so they don't show up at all. You can also have the opposite problem that cells that should not be in the frame at all are in the view but transformed in a way that cannot be seen by the user.
You haven't explained enough of what effect you are trying to do and why you think inheriting from UIFlowLayout is correct for me to be able to specifically help you. But I hope that I have given you enough information that you can find the problem on your own.
The bug is on how you defined frame.origin.y for each attribute. More specifically the value you hold in minY and determines how many of the cells you're keeping on screen. (I will edit this answer and explain more later but for now, try replacing the following code)
var minY = collectionView!.bounds.minY + collectionView!.contentInset.top
let maxY = attributes.frame.origin.y
if minY > attributes.frame.origin.y + attributes.bounds.height + minimumLineSpacing + collectionView!.contentInset.top {
minY = 0
}
Native contacts app has interesting effect - when user tries to scroll, the scroll view pushes the top view with avatar and only when top view is in "small mode" it is scrolled up.
I was able to resize view on scrolling, via didScroll method. But problem is, content offset is also changing while i push the top vied. In native contacts, content offset changes only when top view is in "small mode"
Any suggestions, how did they made this?
Here's a sample project of how you could do it:
https://github.com/lukaswuerzburger/resizeable-tableview-header
You simply have a view controller with a table view and a custom view (resizable header) in it. On scrollViewDidScroll you change the height constraint.
PS: The transitions between the master and detail view controller are not prefect, but maybe someone can help me with that. The change of navigationBar's backgroundImage and shadowImage doesn't really work animated when popping back to the master view controller.
The bounty answer is good, but it still gives somewhat choppy solution to this problem. If you want exacly like native this is your solution. I have been playing a lot lately with collection views, and have gained much more experience. One of the things that I found is that this problem can easily be solved with custom layout:
class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
let minHeightForHeader: CGFloat = 50
let offsetFromTop: CGFloat = 20
override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
// get current attributes
let attributes = super.layoutAttributesForSupplementaryView(ofKind: elementKind, at: indexPath)
// get first section
if let attributes = attributes, elementKind == UICollectionElementKindSectionHeader && indexPath.section == 0 {
if let collectionView = collectionView {
// now check for content offset
var frame = attributes.frame
let yOffset = collectionView.contentOffset.y
if yOffset >= -offsetFromTop {
let calculatedHeight = frame.size.height - (yOffset + offsetFromTop)
let maxValue = minHeightForHeader > calculatedHeight ? minHeightForHeader : calculatedHeight
frame.size.height = maxValue
attributes.frame = frame
}
else {
frame.origin.y = offsetFromTop + yOffset
attributes.frame = frame
}
}
return attributes
}
// default return
return attributes
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
}
Just replace your default layout with this custom class and add this line somewhere in your setup method
flowLayout.sectionHeadersPinToVisibleBounds = true
Voila!
EDIT: Just one more optional method for clipping part, when you scroll to certain part, scroll might continue or return:
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
if let collectionView = collectionView {
let yOffset = collectionView.contentOffset.y
if yOffset + offsetFromTop >= maxHeightForHeader / 2 && yOffset + offsetFromTop < maxHeightForHeader && !(velocity.y < 0) || velocity.y > 0{
return CGPoint.init(x: 0, y: maxHeightForHeader - minHeightForHeader - offsetFromTop)
}
if yOffset + offsetFromTop < maxHeightForHeader / 2 && yOffset + offsetFromTop > 0 || velocity.y < 0 {
return CGPoint.init(x: 0, y: -offsetFromTop)
}
}
return proposedContentOffset
}
I am trying to animate the tableviews height when the collection view scrolls. I call the animation function from scrollViewWillBeginDragging. Then I execute an animation block:
func scrollViewWillBeginDragging(scrollView: UIScrollView) {
if expanded == false && scrollView == IBcalendarCollectionView {
expanded = true
expandCollectionView()
}
}
func expandCollectionView() {
let screenSize: CGRect = UIScreen.mainScreen().bounds
UIView.animateWithDuration(3, delay: 0, options: UIViewAnimationOptions.TransitionNone, animations: { () -> Void in
self.IBcalendarTableView.frame = CGRectMake(self.IBcalendarTableView.frame.origin.x, screenSize.height/2, self.IBcalendarTableView.frame.width, screenSize.height/2)
}) { (success) -> Void in
print(self.IBcalendarTableView.frame.origin.y)
}
}
Whats happening is the TableView animates up to the status bar then returns to its original position. I want it to animate down to half the size of the screen. The animation block is only executed once because I set a boolean when the animation function is called.
you say you set a boolean when the animation block is called, is that so the animation is not being called again and again? I also don't see that in your code.
This should work fine.
func scrollViewWillBeginDragging(scrollView: UIScrollView) {
let mainScreen = UIScreen.mainScreen().bounds
if (self.IBcalendarTableView.frame.origin.y != mainScreen.height / 2) {
var frame = self.IBcalendarTableView.frame
frame.origin.y = mainScreen.height / 2
frame.size.height = frame.origin.y
UIView.animateWithDuration(0.5) { () -> Void in
self.IBcalendarTableView.frame = frame
}
}
}
I set a lower animation speed, 3 seconds seems like a lot... I also built the frame differently, I just do it because I find it easier to read.
Please let me know if there is anything else I can do to help.
The issue is with autolayout. You have to also set the height constraint of the tableview in order to keep the tableview at the new height/origin.
For anyone else who may have this issue:
Dynamic UITableView height
This answer helped me solve this problem. My fixed code looks like this:
func expandCollectionView() {
let screenSize: CGRect = UIScreen.mainScreen().bounds
var frame = self.IBcalendarTableView.frame
frame.origin.y = screenSize.height / 2
frame.size.height = frame.origin.y
UIView.animateWithDuration(0.5) { () -> Void in
self.IBcalendarTableView.frame = frame
self.IBtableViewHeightConstraint.constant = screenSize.height/2
}
}
I am trying to modify UICollectionViewFlowLayout (vertical scroll) in order to place each section header to the left of all items of that section (as opposed to on top, which is the default).
That is, this is the default behaviour:
...and this is what I want:
So I subclassed UICollectionViewFlowLayout:
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let attributesToReturn = super.layoutAttributesForElementsInRect(rect) else {
return nil
}
// Copy to avoid the dreadded "Cached frame mismatch" runtime warning:
var copiedAttributes = [UICollectionViewLayoutAttributes]()
for attribute in attributesToReturn {
copiedAttributes.append(attribute.copy() as! UICollectionViewLayoutAttributes)
}
for attributes in copiedAttributes {
if let kind = attributes.representedElementKind {
// Non nil: It is a supplementary View
if kind == UICollectionElementKindSectionHeader {
// HEADER
var frame = attributes.frame
frame.origin.y = frame.origin.y + frame.size.height
frame.size.width = sectionInset.left
attributes.frame = frame
}
}
else{
// Nil: It is an item
}
}
return copiedAttributes
}
Also, for good measure (?), I adopted the protocol UICollectionViewDelegateFlowLayout and implemented this method (although it is not clear what takes precedence. And then, there is the settings in the storyboard file, but those seem to be overriden by their runtime counterparts):
func collectionView(
collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
referenceSizeForHeaderInSection section: Int) -> CGSize {
let left = (collectionViewLayout as! UICollectionViewFlowLayout).sectionInset.left
return CGSizeMake(left, 1)
}
...and I succeed in lowering the header to the first row of its section; However, the space originally occupied by the header stays open:
...and the only way I can accomplish that is by setting the header view height to 1 and "Clips Subviews" to false, so that the label is displayed (If I set the height to 0, the label is not drawn), but this is definitely not the most elegant solution (and will likely break in -say- iOS 9.2)
That is, the actual height of the header is linked to the vertical space between sections: I can not set the space to zero while keeping the header view at a reasonable size for display.
Perhaps I should also move all section items up (by the same amount as my header height) instead, to fill the hole?
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray *originalAttributes = [super layoutAttributesForElementsInRect:rect];
NSMutableArray *allAttributes = [NSMutableArray new];
for (UICollectionViewLayoutAttributes* attributes in originalAttributes) {
[allAttributes addObject:[attributes copy]];
}
for (UICollectionViewLayoutAttributes *attributes in allAttributes) {
NSString *kind = attributes.representedElementKind;
if (kind == UICollectionElementKindSectionHeader) {
CGRect frame = attributes.frame;
UICollectionViewLayoutAttributes *cellAttrs = [super layoutAttributesForItemAtIndexPath:attributes.indexPath];
frame.origin.x = frame.origin.x;
frame.size.height = self.sectionInset.top;
frame.size.width = cellAttrs.frame.size.width;
attributes.frame = frame;
}
}
return allAttributes;
}
OK, so this is what I did:
First of all, I subclassed UICollectionViewFlowLayout based on this github project, to get the left alignment I was looking for (I had to convert it from Objective-C to swift, but other than than it's pretty much the same).
Since I am already subclassing the layout object, I can implement any modifications in this subclass.
I declared a property to store the width of my "side-headers" (this quantity also doubles as the left inset of each section):
import UIKit
class LeftAlignedFlowLayout: UICollectionViewFlowLayout
{
let customHeaderWidth:CGFloat = 150.0
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
sectionInset.left = customHeaderWidth
}
Then, in the implementation of layoutAttributesForElementsInRect() I did this:
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]?
{
guard let attributesToReturn = super.layoutAttributesForElementsInRect(rect) else {
return nil
}
var copiedAttributes = [UICollectionViewLayoutAttributes]()
for attribute in attributesToReturn {
// Must copy attributes to avoid runtime warning:
copiedAttributes.append(attribute.copy() as! UICollectionViewLayoutAttributes)
}
for attributes in copiedAttributes {
if let kind = attributes.representedElementKind {
// Non nil : Supplementary View
if kind == UICollectionElementKindSectionHeader {
// [A] HEADER
var frame = attributes.frame
frame.origin.y = frame.origin.y + frame.size.height
frame.size.width = sectionInset.left
frame.size.height = 60 // Hard-coded - header height
attributes.frame = frame
}
else if kind == UICollectionElementKindSectionFooter {
// [B] FOOTER
var frame = attributes.frame
// My footer view is a "hairline" taking most of the width
// (save for 8 points of inset margin on each side):
frame.origin.x += 8
frame.size.width -= 16
attributes.frame = frame
}
}
else{
// Kind is nil : Item (cell)
if let attributesForItem = self.layoutAttributesForItemAtIndexPath(attributes.indexPath){
attributes.frame = attributesForItem.frame
}
}
}
return copiedAttributes
}
There is also the implementation of layoutAttributesForItemAtIndexPath(), but that pertains more to the left alignment of the items-part, so I omitted it (if anyone is interested, please check the above-linked github project).
I will create a repository with a demo project of my implementation as soon as I have time.
These are the Size Inspector settings for the collection view and the flow layout object, respectively:
(By the way, item size is variable and determined at runtime by the delegate method, so ignore those values)
I also adopted the UICollectionViewDelegateFlowLayout protocol, like this:
func collectionView(collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
referenceSizeForHeaderInSection section: Int) -> CGSize
{
let left = (collectionViewLayout as! UICollectionViewFlowLayout).sectionInset.left
return CGSizeMake(left, 1)
}
Although, to be honest, some of the layout attributes can be specified in many places (Interface Builder, properties of layout object, return value from delegate methods, etc... ) and I don't remember exactly which takes precendence in each case, so I need to clean up my code a bit.
I need a UICollectionView to display a grid that is potentially larger than the visible frame in both width and height, while maintaining row and column integrity. The default UICollectionViewFlowLayout allows sections to scroll off-screen, but it wraps items within a section to keep them all on-screen, which screws up my grid.
Recognizing that UICollectionView is a subclass of UIScrollView, I tried just manually setting the collection view's content size property in viewDidLoad:
self.collectionView.contentSize = CGSizeMake((columns * (cellWidth + itemSpacingX), (rows * (cellHeight + itemSpacingY));
But this had no effect. Questions:
Is there an easy way to accomplish this without building a custom layout?
Would using a custom layout and overriding the collectionViewContentSize method succeed in getting the collection view to stop wrapping items and scroll in both directions?
If I do have to build a custom layout--which I'll have to invest some time to learn--do I subclass UICollectionViewLayout, or would subclassing UICollectionViewFlowLayout save time?
UPDATE:
I tried embedding the UICollectionView as a subview of a UIScrollView. The collection view itself is behaving correctly--the rows aren't wrapping at the edge of the scroll view, telling me that it is filling the UIScrollView content size that I set. But the scroll view doesn't pan in the horizontal direction, i.e. it only scrolls vertically, which the collection view does by itself anyway. So stuck again. Could there be an issue with the responder chain?
Figured out two ways to do this. Both required a custom layout. The problem is that the default flow layout--and I now know from the Collection View Programming Guide this is partly the definition of a flow layout--generates the cell layout attributes based on the bounds of the superview, and will wrap items in a section to keep them in bounds so that scrolling occurs in only one axis. Will skip the code details, as it isn't hard, and my problem was mainly confusion on what approach to take.
Easy way: use a UIScrollView and subclass 'UICollectionViewFlowLayout'. Embed the UICollectionView in a UIScrollView. Set the contentSize property of the scroll view in viewDiDLoad to match the full size that your collection view will occupy (this will let the default flow layout place items in a single line within a section without wrapping). Subclass UICollectionViewFlowLayout, and set that object as your custom layout for the collection view. In the custom flow layout, override collectionViewContentSize to return the full size of the collection view matrix. With this approach, you'll be using a flow layout, but will be able to scroll in both directions to view un-wrapped sections. The disadvantage is that you still have a flow layout that is pretty limited. Plus, it seems clunky to put a UICollectionView inside an instance of its own superclass just to get the functionality that the collection view by itself should have.
Harder way, but more versatile and elegant: subclass UICollectionViewLayout. I used this tutorial to learn how to implement a complete custom layout. You don't need a UIScrollView here. If you forego the flow layout, subclass UICollectionViewLayout, and set that as the custom layout, you can build out the matrix and get the right behavior from the collection view itself. It's more work because you have to generate all the layout attributes, but you'll be positioned to make the collection view do whatever you want.
In my opinion, Apple should add a property to the default flow layout that suppresses wrapping. Getting a device to display a 2D matrix with intact rows and columns isn't an exotic functionality and it seems like it should be easier to do.
Here is a version of AndrewK's code updated to Swift 4:
import UIKit
class CollectionViewMatrixLayout: UICollectionViewLayout {
var itemSize: CGSize
var interItemSpacingY: CGFloat
var interItemSpacingX: CGFloat
var layoutInfo: [IndexPath: UICollectionViewLayoutAttributes]
override init() {
itemSize = CGSize(width: 50, height: 50)
interItemSpacingY = 1
interItemSpacingX = 1
layoutInfo = [IndexPath: UICollectionViewLayoutAttributes]()
super.init()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepare() {
guard let collectionView = self.collectionView else {
return
}
var cellLayoutInfo = [IndexPath: UICollectionViewLayoutAttributes]()
var indexPath = IndexPath(item: 0, section: 0)
let sectionCount = collectionView.numberOfSections
for section in 0..<sectionCount {
let itemCount = collectionView.numberOfItems(inSection: section)
for item in 0..<itemCount {
indexPath = IndexPath(item: item, section: section)
let itemAttributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
itemAttributes.frame = frameForCell(at: indexPath)
cellLayoutInfo[indexPath] = itemAttributes
}
self.layoutInfo = cellLayoutInfo
}
}
func frameForCell(at indexPath: IndexPath) -> CGRect {
let row = indexPath.section
let column = indexPath.item
let originX = (itemSize.width + interItemSpacingX) * CGFloat(column)
let originY = (itemSize.height + interItemSpacingY) * CGFloat(row)
return CGRect(x: originX, y: originY, width: itemSize.width, height: itemSize.height)
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
{
var allAttributes = Array<UICollectionViewLayoutAttributes>()
for (_, attributes) in self.layoutInfo {
if (rect.intersects(attributes.frame)) {
allAttributes.append(attributes)
}
}
return allAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return self.layoutInfo[indexPath]
}
override var collectionViewContentSize: CGSize {
guard let collectionView = self.collectionView else {
return .zero
}
let sectionCount = collectionView.numberOfSections
let height = (itemSize.height + interItemSpacingY) * CGFloat(sectionCount)
let itemCount = Array(0..<sectionCount)
.map { collectionView.numberOfItems(inSection: $0) }
.max() ?? 0
let width = (itemSize.width + interItemSpacingX) * CGFloat(itemCount)
return CGSize(width: width, height: height)
}
}
Here is complete matrix customLayout:
import UIKit
class MatrixLayout: UICollectionViewLayout {
var itemSize: CGSize!
var interItemSpacingY: CGFloat!
var interItemSpacingX: CGFloat!
var layoutInfo: Dictionary<NSIndexPath, UICollectionViewLayoutAttributes>!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
itemSize = CGSizeMake(50.0, 50.0)
interItemSpacingY = 1.0
interItemSpacingX = 1.0
}
override func prepareLayout() {
var cellLayoutInfo = Dictionary<NSIndexPath, UICollectionViewLayoutAttributes>()
let sectionCount = self.collectionView?.numberOfSections()
var indexPath = NSIndexPath(forItem: 0, inSection: 0)
for (var section = 0; section < sectionCount; section += 1)
{
let itemCount = self.collectionView?.numberOfItemsInSection(section)
for (var item = 0; item < itemCount; item += 1)
{
indexPath = NSIndexPath(forItem:item, inSection: section)
let itemAttributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
itemAttributes.frame = frameForCellAtIndexPath(indexPath)
cellLayoutInfo[indexPath] = itemAttributes
}
self.layoutInfo = cellLayoutInfo
}
}
func frameForCellAtIndexPath(indexPath: NSIndexPath) -> CGRect
{
let row = indexPath.section
let column = indexPath.item
let originX = (self.itemSize.width + self.interItemSpacingX) * CGFloat(column)
let originY = (self.itemSize.height + self.interItemSpacingY) * CGFloat(row)
return CGRectMake(originX, originY, self.itemSize.width, self.itemSize.height)
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]?
{
var allAttributes = Array<UICollectionViewLayoutAttributes>()
for (index, attributes) in self.layoutInfo
{
if (CGRectIntersectsRect(rect, attributes.frame))
{
allAttributes.append(attributes)
}
}
return allAttributes
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
return self.layoutInfo[indexPath]
}
override func collectionViewContentSize() -> CGSize {
let width:CGFloat = (self.itemSize.width + self.interItemSpacingX) * CGFloat((self.collectionView?.numberOfItemsInSection(0))!)
let height:CGFloat = (self.itemSize.height + self.interItemSpacingY) * CGFloat((self.collectionView?.numberOfSections())!)
return CGSizeMake(width, height)
}
}
sections are rows,
items are columns