Populating UICollectionView in reverse order - ios

I would like to populate UICollectionView in reverse order so that the last item of the UICollectionView fills first and then the second last and so on. Actually I'm applying animation and items are showing up one by one. Therefore, I want the last item to show up first.

Swift 4.2
I found a simple solution and worked for me to show last item first of a collection view:
Inside viewDidLoad() method:
collectionView.transform = CGAffineTransform.init(rotationAngle: (-(CGFloat)(Double.pi)))
and inside collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) method before returning the cell:
cell.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
(optional) Below lines will be necessary to auto scroll and show new item with smooth scroll.
Add below lines after loading new data:
if self.dataCollection.count > 0 {
self.collectionView.scrollToItem(at: //scroll collection view to indexpath
NSIndexPath.init(row:(self.collectionView?.numberOfItems(inSection: 0))!-1, //get last item of self collectionview (number of items -1)
section: 0) as IndexPath //scroll to bottom of current section
, at: UICollectionView.ScrollPosition.bottom, //right, left, top, bottom, centeredHorizontally, centeredVertically
animated: true)
}

I'm surprised that Apple scares people away from writing their own UICollectionViewLayout in the documentation. It's really very straightforward. Here's an implementation that I just used in an app that will do exactly what are asking. New items appear at the bottom, and the while there is not enough content to fill up the screen the the items are bottom justified, like you see in message apps. In other words item zero in your data source is the lowest item in the stack.
This code assumes that you have multiple sections, each with items of a fixed height and no spaces between items, and the full width of the collection view. If your layout is more complicated, such as different spacing between sections and items, or variable height items, Apple's intention is that you use the prepare() callback to do the heavy lifting and cache size information for later use.
This code uses Swift 3.0.
//
// Created by John Lyon-Smith on 1/7/17.
// Copyright © 2017 John Lyon-Smith. All rights reserved.
//
import Foundation
import UIKit
class InvertedStackLayout: UICollectionViewLayout {
let cellHeight: CGFloat = 100.00 // Your cell height here...
override func prepare() {
super.prepare()
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttrs = [UICollectionViewLayoutAttributes]()
if let collectionView = self.collectionView {
for section in 0 ..< collectionView.numberOfSections {
if let numberOfSectionItems = numberOfItemsInSection(section) {
for item in 0 ..< numberOfSectionItems {
let indexPath = IndexPath(item: item, section: section)
let layoutAttr = layoutAttributesForItem(at: indexPath)
if let layoutAttr = layoutAttr, layoutAttr.frame.intersects(rect) {
layoutAttrs.append(layoutAttr)
}
}
}
}
}
return layoutAttrs
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let layoutAttr = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let contentSize = self.collectionViewContentSize
layoutAttr.frame = CGRect(
x: 0, y: contentSize.height - CGFloat(indexPath.item + 1) * cellHeight,
width: contentSize.width, height: cellHeight)
return layoutAttr
}
func numberOfItemsInSection(_ section: Int) -> Int? {
if let collectionView = self.collectionView,
let numSectionItems = collectionView.dataSource?.collectionView(collectionView, numberOfItemsInSection: section)
{
return numSectionItems
}
return 0
}
override var collectionViewContentSize: CGSize {
get {
var height: CGFloat = 0.0
var bounds = CGRect.zero
if let collectionView = self.collectionView {
for section in 0 ..< collectionView.numberOfSections {
if let numItems = numberOfItemsInSection(section) {
height += CGFloat(numItems) * cellHeight
}
}
bounds = collectionView.bounds
}
return CGSize(width: bounds.width, height: max(height, bounds.height))
}
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
if let oldBounds = self.collectionView?.bounds,
oldBounds.width != newBounds.width || oldBounds.height != newBounds.height
{
return true
}
return false
}
}

Just click on UICollectionView in storyboard,
in inspector menu under view section change semantic to Force Right-to-Left
I have attach an image to show how to do it in the inspector menu:

I'm assuming you are using UICollectionViewFlawLayout, and this doesn't have logic to do that, it only works in a TOP-LEFT BOTTOM-RIGHT order. To do that you have to build your own layout, which you can do creating a new object that inherits from UICollectionViewLayout.
It seems like a lot of work but is not really that much, you have to implement 4 methods, and since your layout is just bottom-up should be easy to know the frames of each cell.
Check the apple tutorial here: https://developer.apple.com/library/prerelease/ios/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/CreatingCustomLayouts/CreatingCustomLayouts.html

The data collection does not actually have to be modified but that will produce the expected result. Since you control the following method:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
Simply return cells created from inverting the requested index. The index path is the cell's index in the collection, not necessarily the index in the source data set. I used this for a reversed display from a CoreData set.
let desiredIndex = dataProfile!.itemEntries!.count - indexPath[1] - 1;

Don't know if this still would be useful but I guess it might be quite useful for others.
If your collection view's cells are of the same height there is actually a much less complicated solution for your problem than building a custom UICollectionViewLayout.
Firstly, just make an outlet of your collection view's top constraint and add this code to the view controller:
-(void)viewWillAppear:(BOOL)animated {
[self.view layoutIfNeeded]; //for letting the compiler know the actual height and width of your collection view before we start to operate with it
if (self.collectionView.frame.size.height > self.collectionView.collectionViewLayout.collectionViewContentSize.height) {
self.collectionViewTopConstraint.constant = self.collectionView.frame.size.height - self.collectionView.collectionViewLayout.collectionViewContentSize.height;
}
So basically you calculate the difference between collection view's height and its content only if the view's height is bigger. Then you adjust it to the constraint's constant. Pretty simple. But if you need to implement cell resizing as well, this code won't be enough. But I guess this approach may be quite useful. Hope this helps.

A simple working solution is here!
// Change the collection view layer transform.
collectionView.transform3D = CATransform3DMakeScale(1, -1, 1)
// Change the cell layer transform.
cell.transform3D = CATransform3DMakeScale(1, -1, 1)

It is as simple as:
yourCollectionView.inverted = true
PS : Same for Texture/IGListKit..

Related

UI CollectionView in UICollectionView Cell Programmatically

Hi I am trying to make a home feed like facebook using UICollectionView But in each cell i want to put another collectionView that have 3 cells.
you can clone the project here
I have two bugs the first is when i scroll on the inner collection View the bounce do not bring back the cell to center. when i created the collection view i enabled the paging and set the minimumLineSpacing to 0
i could not understand why this is happening. when i tried to debug I noticed that this bug stops when i remove this line
layout.estimatedItemSize = CGSize(width: cv.frame.width, height: 1)
but removing that line brings me this error
The behavior of the UICollectionViewFlowLayout is not defined because: the item height must be less than the height of the UICollectionView minus the section insets top and bottom values, minus the content insets top and bottom values
because my cell have a dynamic Height
here is an example
my second problem is the text on each inner cell dosent display the good text i have to scroll until the last cell of the inner collection view to see the good text displayed here is an example
You first issue will be solved by setting the minimumInteritemSpacing for the innerCollectionView in the OuterCell. So the definition for innerCollectionView becomes this:
let innerCollectionView : UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 0
layout.minimumInteritemSpacing = 0
let cv = UICollectionView(frame :.zero , collectionViewLayout: layout)
cv.translatesAutoresizingMaskIntoConstraints = false
cv.backgroundColor = .orange
layout.estimatedItemSize = CGSize(width: cv.frame.width, height: 1)
cv.isPagingEnabled = true
cv.showsHorizontalScrollIndicator = false
return cv
}()
The second issue is solved by adding calls to reloadData and layoutIfNeeded in the didSet of the post property of OuterCell like this:
var post: Post? {
didSet {
if let numLikes = post?.numLikes {
likesLabel.text = "\(numLikes) Likes"
}
if let numComments = post?.numComments {
commentsLabel.text = "\(numComments) Comments"
}
innerCollectionView.reloadData()
self.layoutIfNeeded()
}
}
What you are seeing is related to cell reuse. You can see this in effect if you scroll to the yellow bordered text on the first item and then scroll down. You will see others are also on the yellow bordered text (although at least with the correct text now).
EDIT
As a bonus here is one method to remember the state of the cells.
First you need to track when the position changes so in OuterCell.swft add a new protocol like this:
protocol OuterCellProtocol: class {
func changed(toPosition position: Int, cell: OutterCell)
}
then add an instance variable for a delegate of that protocol to the OuterCell class like this:
public weak var delegate: OuterCellProtocol?
then finally you need to add the following method which is called when the scrolling finishes, calculates the new position and calls the delegate method to let it know. Like this:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if let index = self.innerCollectionView.indexPathForItem(at: CGPoint(x: self.innerCollectionView.contentOffset.x + 1, y: self.innerCollectionView.contentOffset.y + 1)) {
self.delegate?.changed(toPosition: index.row, cell: self)
}
}
So that's each cell detecting when the collection view cell changes and informing a delegate. Let's see how to use that information.
The OutterCellCollectionViewController is going to need to keep track the position for each cell in it's collection view and update them when they become visible.
So first make the OutterCellCollectionViewController conform to the OuterCellProtocol so it is informed when one of its
class OutterCellCollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout, OuterCellProtocol {
then add a class instance variable to record the cell positions to OuterCellCollectionViewController like this:
var positionForCell: [Int: Int] = [:]
then add the required OuterCellProtocol method to record the cell position changes like this:
func changed(toPosition position: Int, cell: OutterCell) {
if let index = self.collectionView?.indexPath(for: cell) {
self.positionForCell[index.row] = position
}
}
and finally update the cellForItemAt method to set the delegate for a cell and to use the new cell positions like this:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "OutterCardCell", for: indexPath) as! OutterCell
cell.post = posts[indexPath.row]
cell.delegate = self
let cellPosition = self.positionForCell[indexPath.row] ?? 0
cell.innerCollectionView.scrollToItem(at: IndexPath(row: cellPosition, section: 0), at: .left, animated: false)
print (cellPosition)
return cell
}
If you managed to get that all setup correctly it should track the positions when you scroll up and down the list.

Continuously center UICollectionView cells both vertically and horizontally whenever new ones appear

The Setup:
I currently have a UICollectionView that is a bunch of circles for cells to mimic the UI of the Apple Watch home screen. I made it by following this guide. In case you didn't read the article or it goes down for some reason, it's basically a UICollectionView of circles with the outer ones being scaled down as you scroll them away from the center.
The Problem:
I want bubbles to pop up dynamically such that whenever a user earns an award, the award "pops" i.e. animates in, scaling from 0 to 1. However, I would like whatever cells there may be to be always centered. So when for example, the user jumps from 1 award to two, I want the first one to move to the left while the new one pops in.
What I've Tried:
I have tried implementing the UICollectionView's insetForSectionAt method, but it doesn't seem to work. I suspect it's because the CollectionView is not one with FlowLayout. I tried turning it into one but I ended up crashing everything...
The Code:
Here is what I have implemented, just in case the article goes down. (condensed for ease of reading):
class CollectionViewLayout: UICollectionViewLayout {
//Invalidate the layout every time the bounds change so we can do the cool changes
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
override var collectionViewContentSize: CGSize {
return CGSize(width: self.itemSize * CGFloat(COLS),
height: self.itemSize * CGFloat(ROWS))
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var attributes: [UICollectionViewLayoutAttributes] = []
for i in 0..<cellCount {
let indexPath = IndexPath(item: i, section: 0)
attributes.append(self.layoutAttributesForItem(at: indexPath)!)
}
return attributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
var attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
// **Code where I create "z" variable by using equation in article, where "z" is just a value [0,1] that determines cell's scale**
// **More code where I Calculate "x" and "y" based on indexPath like in article**
//Set the cell's attributes:
attributes.transform = CGAffineTransform(scaleX: z, y: z)
attributes.size = CGSize(width: self.itemSize, height: self.itemSize)
attributes.center = CGPoint(x: x, y: y)
return attributes
}
}
I believe a solution to your problem would be to scroll the collection view to the indexPath of what award you want to be centered since a collection view is a subclass of a scroll view. You can scroll the collection view to the item via:
collectionView?.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
where indexPath is the indexPath that you would like to scroll to. This automatically animates your scroll, however if you'd like to customize you can call the UIView.animate function

Create Views On Top Of Collection View?

Currently, I have a calendar grid system. I am trying to implement a feature, where I can show event blocks within the calendar. So, I want to be able to implement where it is possible to add the "Untitled" blocks anywhere within the grid system by the user tapping a cell. My first thought is to add a view outside of the UICollectionView, but then when I scroll away on the calendar. The "Untitled" block would still exist, and would still be on the screen. I need to add a cell within the collection layout in order for it to stay within the flow of the collection view. To build the grid system, I had to make a custom subclass of UICollectionViewLayout, so I am not using a UICollectionViewFlowLayout. I am still a little lost how to add a cell above another cell, any ideas on the best way to implement this feature?
I ended up figuring out the answer to my own question. And, I was able to do it without having to hack the collectionView with a bunch of inner scroll views. Here's what I did. I already had my grid system in place, so I had to add an extra section to my collection view. This section's number of items was dependent upon my events data source array as such:
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if section == Constants.numberOfSections {
//custom event section
return events.count
}
//calender grid items
return Constants.numberOfColumns
}
But, my custom layout for the collection view needed to be updated about the cellAttributes. If I did not tell it that a new cell was added, then it would either crash because it couldn't find a corresponding grid member or add the cell to the grid, like it was another part of the grid. So, I had to update the cellAttributes in my custom layout class, and then manually calculate where the cell should be placed within the layout. So, basically every time I add an event cell, the layout has to manually calculate where that cell should be within the grid. This is the function where I manually calculate the events coordinates in my custom layout subclass (the relevant parts is in the custom events comments):
fileprivate func setCellAttributes(item: Int, section: Int) {
// Build the UICollectionVieLayoutAttributes for the cell.
let cellIndex = IndexPath(item: item, section: section)
var cellWidth: Double = CELL_WIDTH
var cellHeight: Double = CELL_HEIGHT
var xPos: Double = 0
var yPos = Double(section) * CELL_HEIGHT
if section == collectionView!.numberOfSections - 1 {
//custom event items
let rect = getCustomEventRect(item: item)
xPos = Double(rect.x)
yPos = Double(rect.y)
cellHeight = Double(rect.height)
cellWidth = Double(rect.width)
} else if item == 0 {
//the y axis cells
cellWidth = yAxisCellWidth
} else {
//all other cells
xPos = calculateXPos(item: item)
}
let cellAttributes = UICollectionViewLayoutAttributes(forCellWith: cellIndex)
cellAttributes.frame = CGRect(x: xPos, y: yPos, width: cellWidth, height: cellHeight)
// Determine zIndex based on cell type.
if section == 0 && item == 0 {
//top left corner cell
cellAttributes.zIndex = 5
} else if section == 0 {
//y axis cells
cellAttributes.zIndex = 4
} else if section == collectionView!.numberOfSections - 1 {
//custom event cells
cellAttributes.zIndex = 2
} else if item == 0 {
//top x axis cells
cellAttributes.zIndex = 3
} else {
//all background schedule cells
cellAttributes.zIndex = 1
}
// Save the attributes.
cellAttrsDictionary[cellIndex] = cellAttributes
}
Also, every time I updated the events in my viewController, I had to update the events in my layout. So, I implemented this in my viewController:
var events: [CustomEvent] = [] {
didSet {
if let layout = theCollectionView.collectionViewLayout as? ScheduleCollectionViewLayout {
layout.events = events
}
}
}
And, when a user taps to add a new event, I make sure to update the layout accordingly:
func removeEventCell(at indexPath: IndexPath) {
let eventSection: Int = collectionView!.numberOfSections - 1
let totalEventItems: Int = collectionView!.numberOfItems(inSection: eventSection)
//decrementing all indexPaths above the deleted event cell, so the attribute dictionary will be up to date, when reloadSections is run by the collectionView.
for item in 0..<totalEventItems where item > indexPath.item {
let targetIndexPath = IndexPath(item: item - 1, section: eventSection)
let cellAttr = cellAttrsDictionary[IndexPath(item: item, section: eventSection)]
cellAttr?.indexPath = targetIndexPath
cellAttrsDictionary[targetIndexPath] = cellAttr
}
let lastIndexPath = IndexPath(item: totalEventItems - 1, section: eventSection)
cellAttrsDictionary.removeValue(forKey: lastIndexPath)
}
fileprivate func addEventCellAttributes(numOfEventsToAdd: Int) {
for num in 1...numOfEventsToAdd {
setCellAttributes(item: events.count - num, section: collectionView!.numberOfSections - 1)
}
}
This is a very manual process of calculating where to put the cells, but it keeps the collectionView working smoothly and allows the functionality of having cells above cells. The summary of my answer is that I added a new section for my custom events, and I had to manually calculate where the positions of these cells should be, rather than just having them flow with the grid system. My interactive calendar now works perfectly. If you're having trouble trying to even create a grid system, I used this tutorial:
https://www.credera.com/blog/mobile-applications-and-web/building-a-multi-directional-uicollectionview-in-swift/

Horizontal footer in a collection view

I'm working in a CollectionView which is horizontal:
I can do this easily with a CollectionView except for the label '6 day streak', I was thinking that view is a footer but in a horizontal CollectionView, this appears in the top or in the bottom in a portrait mode.
I was thinking to create a ScrollView under this CollectionView and to move both at same time, but I don't know if it's a horrible idea.
I think this is not possible with Flow Layout and I'm not an expert of Custom Layout, is this posible?
Any ideas? Thanks
It is old question, but I leave solution for some other people who have same problem.
You can make footer always horizontally located in last, subclassing your UICollectionViewFlowLayout and overriding layoutAttributesForElementsInRect().
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
if let layoutAttributesForElementsInRect = super.layoutAttributesForElementsInRect(rect), let collectionView = self.collectionView {
for layoutAttributes in layoutAttributesForElementsInRect {
if layoutAttributes.representedElementKind == UICollectionElementKindSectionFooter {
let section = layoutAttributes.indexPath.section
let numberOfItemsInSection = collectionView.numberOfItemsInSection(section)
if numberOfItemsInSection > 0 {
let lastCellIndexPath = NSIndexPath(forItem: max(0, numberOfItemsInSection - 1), inSection: section)
if let lastCellAttrs = self.layoutAttributesForItemAtIndexPath(lastCellIndexPath) {
var origin = layoutAttributes.frame.origin
origin.x = CGRectGetMaxX(lastCellAttrs.frame)
layoutAttributes.zIndex = 1024
layoutAttributes.frame.origin = origin
layoutAttributes.frame.size = layoutAttributes.frame.size
}
}
}
}
return layoutAttributesForElementsInRect
}
return nil
}

Preventing "wrapping" of items in UICollectionView

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

Resources