CollectionView Flowlayout Customize - ios

I am making a profile picture collectionview like tinder edit profile pictures. I want first cell bigger than others and 2, 3 cells besides first cell and others should like 3, 4, 5.
Any suggestion?
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if indexPath.item == 0 {
return CGSize(width: 213.34, height: 213.34)
} else {
return CGSize(width: 101.66, height:101.66 )
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 6
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
let lbl = cell.viewWithTag(1) as! UILabel
lbl.text = String(format: "%d", indexPath.row + 1)
return cell
}
}

You need to implement an UICollectionViewLayout, I had called it FillingLayout, Note that you can adjust the number of columns and the size of your big cells with the delegate methods
Explanation
You need to add an Array to track your columns heigths and see what is the shortest column that is private var columsHeights : [CGFloat] = [] and you need also an array of (Int,Float) tuple to keep which spaces are available to be filled, I also added a method in the delegate to get the number of columns we want in the collection View and a method to know if a cell can be added or not in a position according their size.
Then if we want to add a cell we check if can be added if not, because the first column is filled we add the space corresponding to column2 in the avaiableSpaces array and when we add the next cell first we check if can be added in any available space if can be added we add and remove the available space.
here is the full code
import UIKit
protocol FillingLayoutDelegate: class {
func collectionView(_ collectionView:UICollectionView, sizeForViewAtIndexPath indexPath:IndexPath) -> Int
// Returns the amount of columns that have to display at that moment
func numberOfColumnsInCollectionView(collectionView:UICollectionView) ->Int
}
class FillingLayout: UICollectionViewLayout {
weak var delegate: FillingLayoutDelegate!
fileprivate var cellPadding: CGFloat = 10
fileprivate var cache = [UICollectionViewLayoutAttributes]()
fileprivate var contentHeight: CGFloat = 0
private var columsHeights : [CGFloat] = []
private var avaiableSpaces : [(Int,CGFloat)] = []
fileprivate var contentWidth: CGFloat {
guard let collectionView = collectionView else {
return 0
}
let insets = collectionView.contentInset
return collectionView.bounds.width - (insets.left + insets.right)
}
var columnsQuantity : Int{
get{
if(self.delegate != nil)
{
return (self.delegate?.numberOfColumnsInCollectionView(collectionView: self.collectionView!))!
}
return 0
}
}
//MARK: PRIVATE METHODS
private func shortestColumnIndex() -> Int{
var retVal : Int = 0
var shortestValue = MAXFLOAT
var i = 0
for columnHeight in columsHeights {
//debugPrint("Column Height: \(columnHeight) index: \(i)")
if(Float(columnHeight) < shortestValue)
{
shortestValue = Float(columnHeight)
retVal = i
}
i += 1
}
//debugPrint("shortest Column index: \(retVal)")
return retVal
}
//MARK: PRIVATE METHODS
private func largestColumnIndex() -> Int{
var retVal : Int = 0
var largestValue : Float = 0.0
var i = 0
for columnHeight in columsHeights {
//debugPrint("Column Height: \(columnHeight) index: \(i)")
if(Float(columnHeight) > largestValue)
{
largestValue = Float(columnHeight)
retVal = i
}
i += 1
}
//debugPrint("shortest Column index: \(retVal)")
return retVal
}
private func canUseBigColumnOnIndex(columnIndex:Int,size:Int) ->Bool
{
if(columnIndex < self.columnsQuantity - (size-1))
{
let firstColumnHeight = columsHeights[columnIndex]
for i in columnIndex..<columnIndex + size{
if(firstColumnHeight != columsHeights[i]) {
return false
}
}
return true
}
return false
}
override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: contentHeight)
}
override func prepare() {
// Check if cache is empty
guard cache.isEmpty == true, let collectionView = collectionView else {
return
}
// Set all column heights to 0
self.columsHeights = []
for _ in 0..<self.columnsQuantity {
self.columsHeights.append(0)
}
for item in 0 ..< collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
let viewSize: Int = delegate.collectionView(collectionView, sizeForViewAtIndexPath: indexPath)
let blockWidth = (contentWidth/CGFloat(columnsQuantity))
let width = blockWidth * CGFloat(viewSize)
let height = width
var columIndex = self.shortestColumnIndex()
var xOffset = (contentWidth/CGFloat(columnsQuantity)) * CGFloat(columIndex)
var yOffset = self.columsHeights[columIndex]
if(viewSize > 1){//Big Cell
if(!self.canUseBigColumnOnIndex(columnIndex: columIndex,size: viewSize)){
// Set column height
for i in columIndex..<columIndex + viewSize{
if(i < columnsQuantity){
self.avaiableSpaces.append((i,yOffset))
self.columsHeights[i] += blockWidth
}
}
// Set column height
yOffset = columsHeights[largestColumnIndex()]
xOffset = 0
columIndex = 0
}
for i in columIndex..<columIndex + viewSize{
if(i < columnsQuantity){
//current height
let currValue = self.columsHeights[i]
//new column height with the update
let newValue = yOffset + height
//space that will remaing in blank, this must be 0 if its ok
let remainder = (newValue - currValue) - CGFloat(viewSize) * blockWidth
if(remainder > 0) {
debugPrint("Its bigger remainder is \(remainder)")
//number of spaces to fill
let spacesTofillInColumn = Int(remainder/blockWidth)
//we need to add those spaces as avaiableSpaces
for j in 0..<spacesTofillInColumn {
self.avaiableSpaces.append((i,currValue + (CGFloat(j)*blockWidth)))
}
}
self.columsHeights[i] = yOffset + height
}
}
}else{
//if there is not avaiable space
if(self.avaiableSpaces.count == 0)
{
// Set column height
self.columsHeights[columIndex] += height
}else{//if there is some avaiable space
yOffset = self.avaiableSpaces.first!.1
xOffset = CGFloat(self.avaiableSpaces.first!.0) * width
self.avaiableSpaces.remove(at: 0)
}
}
print(width)
let frame = CGRect(x: xOffset, y: yOffset, width: width, height: height)
let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = insetFrame
cache.append(attributes)
contentHeight = max(contentHeight, frame.maxY)
}
}
func getNextCellSize(currentCell: Int, collectionView: UICollectionView) -> Int {
var nextViewSize = 0
if currentCell < (collectionView.numberOfItems(inSection: 0) - 1) {
nextViewSize = delegate.collectionView(collectionView, sizeForViewAtIndexPath: IndexPath(item: currentCell + 1, section: 0))
}
return nextViewSize
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var visibleLayoutAttributes = [UICollectionViewLayoutAttributes]()
// Loop through the cache and look for items in the rect
for attributes in cache {
if attributes.frame.intersects(rect) {
visibleLayoutAttributes.append(attributes)
}
}
return visibleLayoutAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cache[indexPath.item]
}
}
UPDATED
You need to setup your viewController as FillingLayoutDelegate
override func viewDidLoad() {
super.viewDidLoad()
collectionView.delegate = self
collectionView.dataSource = self
// Do any additional setup after loading the view.
if let layout = self.collectionView.collectionViewLayout as? FillingLayout
{
layout.delegate = self
}
}
FillingLayoutDelegate implementation in your ViewController
extension ViewController: FillingLayoutDelegate{
func collectionView(_ collectionView:UICollectionView,sizeForViewAtIndexPath indexPath:IndexPath) ->Int{
if(indexPath.row == 0 || indexPath.row == 4)
{
return 2
}
if(indexPath.row == 5)
{
return 3
}
return 1
}
func numberOfColumnsInCollectionView(collectionView:UICollectionView) ->Int{
return 3
}
}
ScreenShot working

You can use UICollectionViewLayout to handle this. Code is given below:
UICollectionViewLayout class to define layout:
class CustomCircularCollectionViewLayout: UICollectionViewLayout {
var itemSize = CGSize(width: 200, height: 150)
var attributesList = [UICollectionViewLayoutAttributes]()
override func prepare() {
super.prepare()
let itemNo = collectionView?.numberOfItems(inSection: 0) ?? 0
let length = (collectionView!.frame.width - 40)/3
itemSize = CGSize(width: length, height: length)
attributesList = (0..<itemNo).map { (i) -> UICollectionViewLayoutAttributes in
let attributes = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: i, section: 0))
attributes.size = self.itemSize
var x = CGFloat(i%3)*(itemSize.width+10) + 10
var y = CGFloat(i/3)*(itemSize.width+10) + 10
if i > 2 {
y += (itemSize.width+10)
attributes.frame = CGRect(x: x, y: y, width: itemSize.width, height: itemSize.height)
} else if i == 0 {
attributes.frame = CGRect(x: x, y: y, width: itemSize.width*2+10, height: itemSize.height*2+10)
} else {
x = itemSize.width*2 + 30
if i == 2 {
y += itemSize.height + 10
}
attributes.frame = CGRect(x: x, y: y, width: itemSize.width, height: itemSize.height)
}
return attributes
}
}
override var collectionViewContentSize : CGSize {
return CGSize(width: collectionView!.bounds.width, height: (itemSize.height + 10)*CGFloat(ceil(Double(collectionView!.numberOfItems(inSection: 0))/3))+(itemSize.height + 20))
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return attributesList
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
if indexPath.row < attributesList.count
{
return attributesList[indexPath.row]
}
return nil
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
Now set this class as your collection view layout:
self.collectionView!.collectionViewLayout = circularLayoutObject
This will display your grid as given below:
Let me know if it works for you.

Related

the UICollectionView cell is centered why?

I had a problem with the collectionView.
In fact, I had this algorithm :
- the first cell's width is screen_width/2
- the second cell's width is : screen_width
- the third cell's width is screen_width/2
- the fourth cell's width is : screen_width.
and so on.
This is my code in the method
- (CGSize)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout *)collectionViewLayout
sizeForItemAtIndexPath:(NSIndexPath *)indexPath:
userStory.typePhotoStory = ( (indexPath.row + 1) % 3) == 0 ? #"landscape" : ((indexPath.row + 1) % 3) == 1 ? #"portrait": #"landscape";
if ([userStory.typePhotoStory isEqualToString:#"portrait"]) {
cellSizeFinalWidth = cellSize / 2 ;
cellSizeFinalHeight = cellSizeFinalWidth + 20 ;
} else {
cellSizeFinalWidth = cellSize + padding ;
cellSizeFinalHeight = cellSize / 2 + 20 ;
}
}
But I see that, The first and the third cell are placed in the center of the screen , not started from the left.
Please help me on this issue?
You can directly use this code,
Create a new file and paste this below code for custom layout.
import UIKit
protocol NewLayoutDelegate: class {
func collectionView(_ collectionView:UICollectionView, SizeOfCellAtIndexPath indexPath:IndexPath) -> CGSize
}
class NewFlowLayout: UICollectionViewFlowLayout {
weak var delegate : NewLayoutDelegate?
fileprivate var numberOfColumns = 1
fileprivate var cellPadding: CGFloat = 6
fileprivate var cache = [UICollectionViewLayoutAttributes]()
fileprivate var contentHeight: CGFloat = 0
fileprivate var contentWidth: CGFloat {
guard let collectionView = collectionView else {
return 0
}
let insets = collectionView.contentInset
return collectionView.bounds.width - (insets.left + insets.right)
}
override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: contentHeight)
}
override func prepare() {
super.prepare()
guard cache.isEmpty == true, let collectionView = collectionView else {
return
}
let xOffset : CGFloat = 0
let column = 0
var yOffset = [CGFloat](repeating: 0, count: numberOfColumns)
for item in 0 ..< collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
let aSize = delegate?.collectionView(collectionView, SizeOfCellAtIndexPath: indexPath)
let height = cellPadding * 2 + (aSize?.height)!
let frame = CGRect(x: xOffset, y: yOffset[column], width: (aSize?.width)!, height: height)
let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = insetFrame
cache.append(attributes)
contentHeight = max(contentHeight, frame.maxY)
yOffset[column] = yOffset[column] + height
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var visibleLayoutAttributes = [UICollectionViewLayoutAttributes]()
for attributes in cache {
if attributes.frame.intersects(rect) {
visibleLayoutAttributes.append(attributes)
}
}
return visibleLayoutAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cache[indexPath.item]
}
}
In your controller file
import UIKit
class NewCell: UICollectionViewCell {
}
class NewViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, NewLayoutDelegate {
#IBOutlet weak var myCollectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
myCollectionView.collectionViewLayout = NewFlowLayout()
if let layout = myCollectionView.collectionViewLayout as? NewFlowLayout {
layout.delegate = self
}
}
// Do size logic here
func collectionView(_ collectionView: UICollectionView, SizeOfCellAtIndexPath indexPath: IndexPath) -> CGSize {
let insets = collectionView.contentInset
if indexPath.item % 2 == 0 {
return CGSize(width: (collectionView.frame.size.width - (insets.left + insets.right))/2,
height: 60)
}
return CGSize(width: collectionView.frame.size.width - (insets.left + insets.right),
height: 60)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "NewCell", for: indexPath) as! NewCell
return cell
}
}
Output

UICollectionViewLayout small and large cell auto layout

I have two cell sizes small (approx. half screen width before spacing and insets) and large (full screen width before insets), these two sizes can be seen in the following image.
I want the UICollectionView to automatically size these cell depending on a size attribute that is given. I have managed to size the cells correctly but I am having trouble getting the cells to layout correctly when there are not two small cells in sequence.
This is what happens when two small cells are in sequence in the array:
This is where this cell should go:
Here is the custom UICollectionViewLayout's prepare function that I have made:
import UIKit
protocol FlexLayoutDelegate: class {
func collectionView(_ collectionView:UICollectionView, sizeForViewAtIndexPath indexPath:IndexPath) -> Int
}
class FlexLayout: UICollectionViewLayout {
weak var delegate: FlexLayoutDelegate!
fileprivate var cellPadding: CGFloat = 10
fileprivate var cache = [UICollectionViewLayoutAttributes]()
fileprivate var contentHeight: CGFloat = 0
fileprivate var contentWidth: CGFloat {
guard let collectionView = collectionView else {
return 0
}
let insets = collectionView.contentInset
return collectionView.bounds.width - (insets.left + insets.right)
}
override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: contentHeight)
}
override func prepare() {
// Check if cache is empty
guard cache.isEmpty == true, let collectionView = collectionView else {
return
}
var yOffset = CGFloat(0)
var xOffset = CGFloat(0)
var column = 0
for item in 0 ..< collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
let viewSize: CGFloat = CGFloat(delegate.collectionView(collectionView, sizeForViewAtIndexPath: indexPath))
let height = cellPadding * 2 + 240
let width = contentWidth / viewSize
if viewSize == 2 {
xOffset = CGFloat(column) * width
} else {
xOffset = 0
}
print(width)
let frame = CGRect(x: xOffset, y: yOffset, width: width, height: height)
let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = insetFrame
cache.append(attributes)
contentHeight = max(contentHeight, frame.maxY)
column = column < 1 ? column + 1 : 0
yOffset = column == 0 || getNextCellSize(currentCell: indexPath.row, collectionView: collectionView) == 1 ? yOffset + height : yOffset
}
}
func getNextCellSize(currentCell: Int, collectionView: UICollectionView) -> Int {
var nextViewSize = 0
if currentCell < (collectionView.numberOfItems(inSection: 0) - 1) {
nextViewSize = delegate.collectionView(collectionView, sizeForViewAtIndexPath: IndexPath(item: currentCell + 1, section: 0))
}
return nextViewSize
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var visibleLayoutAttributes = [UICollectionViewLayoutAttributes]()
// Loop through the cache and look for items in the rect
for attributes in cache {
if attributes.frame.intersects(rect) {
visibleLayoutAttributes.append(attributes)
}
}
return visibleLayoutAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cache[indexPath.item]
}
}
Thanks
You need to add an Array to track your columns heigths and see what is the shortest column that is private var columsHeights : [CGFloat] = []and you need also an array of (Int,Float) tuple to keep which spaces are available to be filled, I also added a method in the delegate to get the number of columns we want in the collection View and a method to know if a cell can be added or not in a position according their size.
Then if we want to add a cell we check if can be added if not, because the first column is filled we add the space corresponding to column2 in the avaiableSpaces array and when we add the next cell first we check if can be added in any available space if can be added we add and remove the available space.
Here is the code
FlexLayoutDelegate Implementation
extension ViewController: FlexLayoutDelegate{
func collectionView(_ collectionView:UICollectionView,sizeForViewAtIndexPath indexPath:IndexPath) ->Int{
if(indexPath.row % 2 == 1)
{
return 1
}
return 2
}
func numberOfColumnsInCollectionView(collectionView:UICollectionView) ->Int{
return 2
}
}
FlexLayout
import UIKit
protocol FlexLayoutDelegate: class {
func collectionView(_ collectionView:UICollectionView, sizeForViewAtIndexPath indexPath:IndexPath) -> Int
// Returns the amount of columns that have to display at that moment
func numberOfColumnsInCollectionView(collectionView:UICollectionView) ->Int
}
class FlexLayout: UICollectionViewLayout {
weak var delegate: FlexLayoutDelegate!
fileprivate var cellPadding: CGFloat = 10
fileprivate var cache = [UICollectionViewLayoutAttributes]()
fileprivate var contentHeight: CGFloat = 0
private var columsHeights : [CGFloat] = []
private var avaiableSpaces : [(Int,CGFloat)] = []
fileprivate var contentWidth: CGFloat {
guard let collectionView = collectionView else {
return 0
}
let insets = collectionView.contentInset
return collectionView.bounds.width - (insets.left + insets.right)
}
var columnsQuantity : Int{
get{
if(self.delegate != nil)
{
return (self.delegate?.numberOfColumnsInCollectionView(collectionView: self.collectionView!))!
}
return 0
}
}
//MARK: PRIVATE METHODS
private func shortestColumnIndex() -> Int{
var retVal : Int = 0
var shortestValue = MAXFLOAT
var i = 0
for columnHeight in columsHeights {
//debugPrint("Column Height: \(columnHeight) index: \(i)")
if(Float(columnHeight) < shortestValue)
{
shortestValue = Float(columnHeight)
retVal = i
}
i += 1
}
//debugPrint("shortest Column index: \(retVal)")
return retVal
}
private func canUseDoubleColumnOnIndex(columnIndex:Int) ->Bool
{
var retVal = false
if(columnIndex < self.columnsQuantity-1)
{
let firstColumnHeight = columsHeights[columnIndex]
let secondColumnHeight = columsHeights[columnIndex + 1]
//debugPrint(firstColumnHeight - secondColumnHeight)
retVal = firstColumnHeight == secondColumnHeight
}
return retVal
}
override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: contentHeight)
}
override func prepare() {
// Check if cache is empty
guard cache.isEmpty == true, let collectionView = collectionView else {
return
}
// Set all column heights to 0
self.columsHeights = []
for _ in 0..<self.columnsQuantity {
self.columsHeights.append(0)
}
var xOffset = CGFloat(0)
var column = 0
for item in 0 ..< collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
let viewSize: CGFloat = CGFloat(delegate.collectionView(collectionView, sizeForViewAtIndexPath: indexPath))
let height = cellPadding * 2 + 240
let width = contentWidth / viewSize
if viewSize == 2 {
xOffset = CGFloat(column) * width
} else {
xOffset = 0
}
var columIndex = self.shortestColumnIndex()
var yOffset = self.columsHeights[columIndex]
if(viewSize == 1){//Double Cell
if(self.canUseDoubleColumnOnIndex(columnIndex: columIndex)){
// Set column height
self.columsHeights[columIndex] = CGFloat(yOffset) + height
self.columsHeights[columIndex + 1] = CGFloat(yOffset) + height
}else{
self.avaiableSpaces.append((columIndex,yOffset))
// Set column height
yOffset += height
xOffset = 0
columIndex = 0
self.columsHeights[columIndex] = CGFloat(yOffset) + height
self.columsHeights[columIndex + 1] = CGFloat(yOffset) + height
}
}else{
//if there is not avaiable space
if(self.avaiableSpaces.count == 0)
{
// Set column height
self.columsHeights[columIndex] = CGFloat(yOffset) + height
}else{//if there is some avaiable space
yOffset = self.avaiableSpaces.first!.1
xOffset = CGFloat(self.avaiableSpaces.first!.0) * width
self.avaiableSpaces.remove(at: 0)
}
}
print(width)
let frame = CGRect(x: xOffset, y: yOffset, width: width, height: height)
let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = insetFrame
cache.append(attributes)
contentHeight = max(contentHeight, frame.maxY)
column = column < 1 ? column + 1 : 0
yOffset = column == 0 || getNextCellSize(currentCell: indexPath.row, collectionView: collectionView) == 1 ? yOffset + height : yOffset
}
}
func getNextCellSize(currentCell: Int, collectionView: UICollectionView) -> Int {
var nextViewSize = 0
if currentCell < (collectionView.numberOfItems(inSection: 0) - 1) {
nextViewSize = delegate.collectionView(collectionView, sizeForViewAtIndexPath: IndexPath(item: currentCell + 1, section: 0))
}
return nextViewSize
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var visibleLayoutAttributes = [UICollectionViewLayoutAttributes]()
// Loop through the cache and look for items in the rect
for attributes in cache {
if attributes.frame.intersects(rect) {
visibleLayoutAttributes.append(attributes)
}
}
return visibleLayoutAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cache[indexPath.item]
}
}
Result

Create a UICollectionView with a reversed flow layout

I want to create a view that resembles a chat view, where the newest message appears at the bottom of the screen. I would like the UICollectionViewCells to be 'pinned' to the bottom of the screen - the opposite of what the default flow layout does. How can I do this?
I've got this code from another stackoverflow answer, but it doesn't work with variable height items (which it needs to):
import Foundation
import UIKit
class InvertedStackLayout: UICollectionViewLayout {
let cellHeight: CGFloat = 100 // 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
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
}
}
Flip your collectionView with a CGAffineTransform so that it starts at the bottom.
collectionView.transform = CGAffineTransform(scaleX: 1, y: -1)
The only problem now is that all your cells display upside down. You then apply the transform to each cell to flip it back upright.
class InvertedCollectionViewFlowLayout: UICollectionViewFlowLayout {
// inverting the transform in the layout, rather than directly on the cell,
// is the only way I've found to prevent cells from flipping during animated
// cell updates
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attrs = super.layoutAttributesForItem(at: indexPath)
attrs?.transform = CGAffineTransform(scaleX: 1, y: -1)
return attrs
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attrsList = super.layoutAttributesForElements(in: rect)
if let list = attrsList {
for i in 0..<list.count {
list[i].transform = CGAffineTransform(scaleX: 1, y: -1)
}
}
return attrsList
}
}
UICollectionView with a reversed flow layout and dynamic cell and header height.
import Foundation
import UIKit
class InvertedFlowLayout: UICollectionViewFlowLayout {
override func prepare() {
super.prepare()
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard super.layoutAttributesForElements(in: rect) != nil else { return nil }
var attributesArrayNew = [UICollectionViewLayoutAttributes]()
if let collectionView = self.collectionView {
for section in 0 ..< collectionView.numberOfSections {
for item in 0 ..< collectionView.numberOfItems(inSection: section) {
let indexPathCurrent = IndexPath(item: item, section: section)
if let attributeCell = layoutAttributesForItem(at: indexPathCurrent) {
if attributeCell.frame.intersects(rect) {
attributesArrayNew.append(attributeCell)
}
}
}
}
for section in 0 ..< collectionView.numberOfSections {
let indexPathCurrent = IndexPath(item: 0, section: section)
if let attributeKind = layoutAttributesForSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, at: indexPathCurrent) {
attributesArrayNew.append(attributeKind)
}
}
}
return attributesArrayNew
}
override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attributeKind = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: elementKind, with: indexPath)
if let collectionView = self.collectionView {
var fullHeight: CGFloat = 0.0
for section in 0 ..< indexPath.section + 1 {
for item in 0 ..< collectionView.numberOfItems(inSection: section) {
let indexPathCurrent = IndexPath(item: item, section: section)
fullHeight += cellHeight(indexPathCurrent) + minimumLineSpacing
}
}
attributeKind.frame = CGRect(x: 0, y: collectionViewContentSize.height - fullHeight - CGFloat(indexPath.section + 1) * headerHeight(indexPath.section) - sectionInset.bottom + minimumLineSpacing/2, width: collectionViewContentSize.width, height: headerHeight(indexPath.section))
}
return attributeKind
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attributeCell = UICollectionViewLayoutAttributes(forCellWith: indexPath)
if let collectionView = self.collectionView {
var fullHeight: CGFloat = 0.0
for section in 0 ..< indexPath.section + 1 {
for item in 0 ..< collectionView.numberOfItems(inSection: section) {
let indexPathCurrent = IndexPath(item: item, section: section)
fullHeight += cellHeight(indexPathCurrent) + minimumLineSpacing
if section == indexPath.section && item == indexPath.item {
break
}
}
}
attributeCell.frame = CGRect(x: 0, y: collectionViewContentSize.height - fullHeight + minimumLineSpacing - CGFloat(indexPath.section) * headerHeight(indexPath.section) - sectionInset.bottom, width: collectionViewContentSize.width, height: cellHeight(indexPath) )
}
return attributeCell
}
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 {
for item in 0 ..< collectionView.numberOfItems(inSection: section) {
let indexPathCurrent = IndexPath(item: item, section: section)
height += cellHeight(indexPathCurrent) + minimumLineSpacing
}
}
height += sectionInset.bottom + CGFloat(collectionView.numberOfSections) * headerHeight(0)
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
}
func cellHeight(_ indexPath: IndexPath) -> CGFloat {
if let collectionView = self.collectionView, let delegateFlowLayout = collectionView.delegate as? UICollectionViewDelegateFlowLayout {
let size = delegateFlowLayout.collectionView!(collectionView, layout: self, sizeForItemAt: indexPath)
return size.height
}
return 0
}
func headerHeight(_ section: Int) -> CGFloat {
if let collectionView = self.collectionView, let delegateFlowLayout = collectionView.delegate as? UICollectionViewDelegateFlowLayout {
let size = delegateFlowLayout.collectionView!(collectionView, layout: self, referenceSizeForHeaderInSection: section)
return size.height
}
return 0
}
}
you could rotate(transform) collectionView 180 degrees so top will be at the bottom and rotate(transform) each cell 180 to make them look normal.

Swift / CollectionViewWaterfallLayout / how to set cell amount

I'm using this CollectionViewWaterfallLayout.
The following functions sets the size of the cells randomly:
var testInteger = 10
lazy var cellSizes: [CGSize] = {
var _cellSizes = [CGSize]()
for _ in 0...10 {
let random = Int(arc4random_uniform((UInt32(100))))
_cellSizes.append(CGSize(width: 140, height: 50 + random))
}
return _cellSizes
}()
This: for _ in 0...10 sets the amount of the cells to 10, but since I'm downloading data from a web blog, I need to set it by var posts = [EventPosts]() -> posts.count, to be the amount of blog entrys that are downloaded. How can I do that?
I've tryed to use for _ in Int(posts.count), but it says, that Instant Member posts, can't be used on type ViewControllerXY.
Help is very appreciated.
Edit. This is the Custom Layout class.
//
// CollectionViewWaterfallLayout.swift
// CollectionViewWaterfallLayout
//
// Created by Eric Cerney on 7/21/14.
// Based on CHTCollectionViewWaterfallLayout by Nelson Tai
// Copyright (c) 2014 Eric Cerney. All rights reserved.
//
import UIKit
public let CollectionViewWaterfallElementKindSectionHeader = "CollectionViewWaterfallElementKindSectionHeader"
public let CollectionViewWaterfallElementKindSectionFooter = "CollectionViewWaterfallElementKindSectionFooter"
#objc public protocol CollectionViewWaterfallLayoutDelegate:UICollectionViewDelegate {
func collectionView(collectionView: UICollectionView, layout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize
optional func collectionView(collectionView: UICollectionView, layout: UICollectionViewLayout, heightForHeaderInSection section: Int) -> Float
optional func collectionView(collectionView: UICollectionView, layout: UICollectionViewLayout, heightForFooterInSection section: Int) -> Float
optional func collectionView(collectionView: UICollectionView, layout: UICollectionViewLayout, insetForSection section: Int) -> UIEdgeInsets
optional func collectionView(collectionView: UICollectionView, layout: UICollectionViewLayout, insetForHeaderInSection section: Int) -> UIEdgeInsets
optional func collectionView(collectionView: UICollectionView, layout: UICollectionViewLayout, insetForFooterInSection section: Int) -> UIEdgeInsets
optional func collectionView(collectionView: UICollectionView, layout: UICollectionViewLayout, minimumInteritemSpacingForSection section: Int) -> Float
}
public class CollectionViewWaterfallLayout: UICollectionViewLayout {
//MARK: Private constants
/// How many items to be union into a single rectangle
private let unionSize = 20;
//MARK: Public Properties
public var columnCount:Int = 2 {
didSet {
invalidateIfNotEqual(oldValue, newValue: columnCount)
}
}
public var minimumColumnSpacing:Float = 10.0 {
didSet {
invalidateIfNotEqual(oldValue, newValue: minimumColumnSpacing)
}
}
public var minimumInteritemSpacing:Float = 10.0 {
didSet {
invalidateIfNotEqual(oldValue, newValue: minimumInteritemSpacing)
}
}
public var headerHeight:Float = 0.0 {
didSet {
invalidateIfNotEqual(oldValue, newValue: headerHeight)
}
}
public var footerHeight:Float = 0.0 {
didSet {
invalidateIfNotEqual(oldValue, newValue: footerHeight)
}
}
public var headerInset:UIEdgeInsets = UIEdgeInsetsZero {
didSet {
invalidateIfNotEqual(NSValue(UIEdgeInsets: oldValue), newValue: NSValue(UIEdgeInsets: headerInset))
}
}
public var footerInset:UIEdgeInsets = UIEdgeInsetsZero {
didSet {
invalidateIfNotEqual(NSValue(UIEdgeInsets: oldValue), newValue: NSValue(UIEdgeInsets: footerInset))
}
}
public var sectionInset:UIEdgeInsets = UIEdgeInsetsZero {
didSet {
invalidateIfNotEqual(NSValue(UIEdgeInsets: oldValue), newValue: NSValue(UIEdgeInsets: sectionInset))
}
}
//MARK: Private Properties
private weak var delegate: CollectionViewWaterfallLayoutDelegate? {
get {
return collectionView?.delegate as? CollectionViewWaterfallLayoutDelegate
}
}
private var columnHeights = [Float]()
private var sectionItemAttributes = [[UICollectionViewLayoutAttributes]]()
private var allItemAttributes = [UICollectionViewLayoutAttributes]()
private var headersAttribute = [Int: UICollectionViewLayoutAttributes]()
private var footersAttribute = [Int: UICollectionViewLayoutAttributes]()
private var unionRects = [CGRect]()
//MARK: UICollectionViewLayout Methods
override public func prepareLayout() {
super.prepareLayout()
let numberOfSections = collectionView?.numberOfSections()
if numberOfSections == 0 {
return;
}
assert(delegate!.conformsToProtocol(CollectionViewWaterfallLayoutDelegate), "UICollectionView's delegate should conform to WaterfallLayoutDelegate protocol")
assert(columnCount > 0, "WaterfallFlowLayout's columnCount should be greater than 0")
// Initialize variables
headersAttribute.removeAll(keepCapacity: false)
footersAttribute.removeAll(keepCapacity: false)
unionRects.removeAll(keepCapacity: false)
columnHeights.removeAll(keepCapacity: false)
allItemAttributes.removeAll(keepCapacity: false)
sectionItemAttributes.removeAll(keepCapacity: false)
for _ in 0..<columnCount {
self.columnHeights.append(0)
}
// Create attributes
var top:Float = 0
var attributes: UICollectionViewLayoutAttributes
for section in 0..<numberOfSections! {
/*
* 1. Get section-specific metrics (minimumInteritemSpacing, sectionInset)
*/
var minimumInteritemSpacing: Float
if let height = delegate?.collectionView?(collectionView!, layout: self, minimumInteritemSpacingForSection: section) {
minimumInteritemSpacing = height
}
else {
minimumInteritemSpacing = self.minimumInteritemSpacing
}
var sectionInset: UIEdgeInsets
if let inset = delegate?.collectionView?(collectionView!, layout: self, insetForSection: section) {
sectionInset = inset
}
else {
sectionInset = self.sectionInset
}
let width = Float(collectionView!.frame.size.width - sectionInset.left - sectionInset.right)
let itemWidth = floorf((width - Float(columnCount - 1) * Float(minimumColumnSpacing)) / Float(columnCount))
/*
* 2. Section header
*/
var headerHeight: Float
if let height = delegate?.collectionView?(collectionView!, layout: self, heightForHeaderInSection: section) {
headerHeight = height
}
else {
headerHeight = self.headerHeight
}
var headerInset: UIEdgeInsets
if let inset = delegate?.collectionView?(collectionView!, layout: self, insetForHeaderInSection: section) {
headerInset = inset
}
else {
headerInset = self.headerInset
}
top += Float(headerInset.top)
if headerHeight > 0 {
attributes = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: CollectionViewWaterfallElementKindSectionHeader, withIndexPath: NSIndexPath(forItem: 0, inSection: section))
attributes.frame = CGRect(x: headerInset.left, y: CGFloat(top), width: collectionView!.frame.size.width - (headerInset.left + headerInset.right), height: CGFloat(headerHeight))
headersAttribute[section] = attributes
allItemAttributes.append(attributes)
top = Float(CGRectGetMaxY(attributes.frame)) + Float(headerInset.bottom)
}
top += Float(sectionInset.top)
for idx in 0..<columnCount {
columnHeights[idx] = top
}
/*
* 3. Section items
*/
let itemCount = collectionView!.numberOfItemsInSection(section)
var itemAttributes = [UICollectionViewLayoutAttributes]()
// Item will be put into shortest column.
for idx in 0..<itemCount {
let indexPath = NSIndexPath(forItem: idx, inSection: section)
let columnIndex = shortestColumnIndex()
let xOffset = Float(sectionInset.left) + Float(itemWidth + minimumColumnSpacing) * Float(columnIndex)
let yOffset = columnHeights[columnIndex]
let itemSize = delegate?.collectionView(collectionView!, layout: self, sizeForItemAtIndexPath: indexPath)
var itemHeight: Float = 0.0
if itemSize?.height > 0 && itemSize?.width > 0 {
itemHeight = Float(itemSize!.height) * itemWidth / Float(itemSize!.width)
}
attributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
attributes.frame = CGRect(x: CGFloat(xOffset), y: CGFloat(yOffset), width: CGFloat(itemWidth), height: CGFloat(itemHeight))
itemAttributes.append(attributes)
allItemAttributes.append(attributes)
columnHeights[columnIndex] = Float(CGRectGetMaxY(attributes.frame)) + minimumInteritemSpacing
}
sectionItemAttributes.append(itemAttributes)
/*
* 4. Section footer
*/
var footerHeight: Float
let columnIndex = longestColumnIndex()
top = columnHeights[columnIndex] - minimumInteritemSpacing + Float(sectionInset.bottom)
if let height = delegate?.collectionView?(collectionView!, layout: self, heightForFooterInSection: section) {
footerHeight = height
}
else {
footerHeight = self.footerHeight
}
var footerInset: UIEdgeInsets
if let inset = delegate?.collectionView?(collectionView!, layout: self, insetForFooterInSection: section) {
footerInset = inset
}
else {
footerInset = self.footerInset
}
top += Float(footerInset.top)
if footerHeight > 0 {
attributes = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: CollectionViewWaterfallElementKindSectionFooter, withIndexPath: NSIndexPath(forItem: 0, inSection: section))
attributes.frame = CGRect(x: footerInset.left, y: CGFloat(top), width: collectionView!.frame.size.width - (footerInset.left + footerInset.right), height: CGFloat(footerHeight))
footersAttribute[section] = attributes
allItemAttributes.append(attributes)
top = Float(CGRectGetMaxY(attributes.frame)) + Float(footerInset.bottom)
}
for idx in 0..<columnCount {
columnHeights[idx] = top
}
}
// Build union rects
var idx = 0
let itemCounts = allItemAttributes.count
while idx < itemCounts {
let rect1 = allItemAttributes[idx].frame
idx = min(idx + unionSize, itemCounts) - 1
let rect2 = allItemAttributes[idx].frame
unionRects.append(CGRectUnion(rect1, rect2))
++idx
}
}
override public func collectionViewContentSize() -> CGSize {
let numberOfSections = collectionView?.numberOfSections()
if numberOfSections == 0 {
return CGSizeZero
}
var contentSize = collectionView?.bounds.size
contentSize?.height = CGFloat(columnHeights[0])
return contentSize!
}
override public func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
if indexPath.section >= sectionItemAttributes.count {
return nil
}
if indexPath.item >= sectionItemAttributes[indexPath.section].count {
return nil
}
return sectionItemAttributes[indexPath.section][indexPath.item]
}
override public func layoutAttributesForSupplementaryViewOfKind(elementKind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
var attribute: UICollectionViewLayoutAttributes?
if elementKind == CollectionViewWaterfallElementKindSectionHeader {
attribute = headersAttribute[indexPath.section]
}
else if elementKind == CollectionViewWaterfallElementKindSectionFooter {
attribute = footersAttribute[indexPath.section]
}
return attribute
}
override public func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var begin:Int = 0
var end: Int = unionRects.count
var attrs = [UICollectionViewLayoutAttributes]()
for i in 0..<unionRects.count {
if CGRectIntersectsRect(rect, unionRects[i]) {
begin = i * unionSize
break
}
}
for i in (0..<unionRects.count).reverse() {
if CGRectIntersectsRect(rect, unionRects[i]) {
end = min((i+1) * unionSize, allItemAttributes.count)
break
}
}
for var i = begin; i < end; i++ {
let attr = allItemAttributes[i]
if CGRectIntersectsRect(rect, attr.frame) {
attrs.append(attr)
}
}
return Array(attrs)
}
override public func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
let oldBounds = collectionView?.bounds
if CGRectGetWidth(newBounds) != CGRectGetWidth(oldBounds!) {
return true
}
return false
}
//MARK: Private Methods
private func shortestColumnIndex() -> Int {
var index: Int = 0
var shortestHeight = MAXFLOAT
for (idx, height) in columnHeights.enumerate() {
if height < shortestHeight {
shortestHeight = height
index = idx
}
}
return index
}
private func longestColumnIndex() -> Int {
var index: Int = 0
var longestHeight:Float = 0
for (idx, height) in columnHeights.enumerate() {
if height > longestHeight {
longestHeight = height
index = idx
}
}
return index
}
private func invalidateIfNotEqual(oldValue: AnyObject, newValue: AnyObject) {
if !oldValue.isEqual(newValue) {
invalidateLayout()
}
}
}
You should override this function:
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes?
And then use this in prepareLayout():
// my collection only have one section,you can for-in all sections
for row in 0..<collectionView!.numberOfItemsInSection(0) {
allLayoutAttributes.append(layoutAttributesForItemAtIndexPath(NSIndexPath(forItem: row, inSection: 0))!)
}
And return all attributes at last :
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return allLayoutAttributes
}

Horizontal Flow for UICollectionView

I want to implement something like this:
I use a collection view with an horizontal flow and custom cell to set the size (every cell has a different size based on the text). The problem is that the collection view is like a matrix and when there is a small element and a big element on the same column, there will be a bigger space between elements from the same line.
Now I have something like this:
Is there any solution to do this with collection view? Or should I use scroll view instead?
Thank you!
For Flow layout Swift
override func viewDidLoad() {
var flowLayout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
flowLayout.estimatedItemSize = CGSizeMake(view.frame.width - 10, 10)
flowLayout.minimumLineSpacing = 2
flowLayout.minimumInteritemSpacing = 2
flowLayout.sectionInset = UIEdgeInsetsMake(2, 2, 0, 0)
collectionView.dataSource=self
collectionView.delegate=self
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
return CGSize(width: 90, height: 50) // The size of one cell
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return CGSizeMake(self.view.frame.width, 0) // Header size
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAtIndex section: Int) -> UIEdgeInsets {
let frame : CGRect = self.view.frame
let margin: CGFloat = 0
return UIEdgeInsetsMake(0, margin, 0, margin) // margin between cells
}
You will have to write Your own custom flow layout subclass or use an already written one ( third party ). The main functions You need to consider for overriding ( from UICollectionViewLayout ) are :
-(void)prepareLayout
-(CGSize)collectionViewContentSize
-(UICollectionViewLayoutAttributes*) layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
-(UICollectionViewLayoutAttributes *) layoutAttributesForSupplementaryViewOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
Also keep in mind that when You use the UICollectionViewFlowLayout the horizontal space between the items is mainly controller from :
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section;
This is the solution that worked for me:
class CustomHorizontalLayout: UICollectionViewFlowLayout
{
var _contentSize = CGSizeZero
var itemAttibutes: NSMutableArray = NSMutableArray()
var delegate: CustomHorizontalLayoutDelegate!
override func prepareLayout() {
super.prepareLayout()
self.itemAttibutes = NSMutableArray()
// use a value to keep track of left margin
var upLeftMargin: CGFloat = self.sectionInset.left;
var downLeftMargin: CGFloat = self.sectionInset.left;
let numberOfItems = self.collectionView!.numberOfItemsInSection(0)
for var item = 0; item < numberOfItems; item++ {
let indexPath = NSIndexPath(forItem: item, inSection: 0)
let refAttributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
let size = delegate.collectionView(collectionView!, sizeTagAtIndexPath: indexPath)
if (refAttributes.frame.origin.x == self.sectionInset.left) {
upLeftMargin = self.sectionInset.left
downLeftMargin = self.sectionInset.left
} else {
// set position of attributes
var newLeftAlignedFrame = refAttributes.frame
newLeftAlignedFrame.origin.x = (indexPath.row % 2 == 0) ? upLeftMargin :downLeftMargin
newLeftAlignedFrame.origin.y = (indexPath.row % 2 == 0) ? 0 :40
newLeftAlignedFrame.size.width = size.width
newLeftAlignedFrame.size.height = size.height
refAttributes.frame = newLeftAlignedFrame
}
// calculate new value for current margin
if(indexPath.row % 2 == 0)
{
upLeftMargin += refAttributes.frame.size.width + 8
}
else
{
downLeftMargin += refAttributes.frame.size.width + 8
}
self.itemAttibutes.addObject(refAttributes)
}
_contentSize = CGSizeMake(max(upLeftMargin, downLeftMargin), 80)
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
return self.itemAttibutes.objectAtIndex(indexPath.row) as? UICollectionViewLayoutAttributes
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let predicate = NSPredicate { (evaluatedObject, bindings) -> Bool in
let layoutAttribute = evaluatedObject as! UICollectionViewLayoutAttributes
return CGRectIntersectsRect(rect, layoutAttribute.frame)
}
return (itemAttibutes.filteredArrayUsingPredicate(predicate) as! [UICollectionViewLayoutAttributes])
}
override func collectionViewContentSize() -> CGSize {
return _contentSize
}
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
return false
}
}
Here is the solution it works fine for a flexible width. Create a new Swift.File paste the code
and then implement the delegate Method in your ViewController.
import UIKit
protocol CameraLayoutDelegate: class {
func collectionView(_ collectionView:UICollectionView, widthForPhotoAtIndexPath indexPath:IndexPath) -> CGFloat
}
class CameraLayout: UICollectionViewLayout {
var delegate: CameraLayoutDelegate!
var numberOfRows = 2
private var cache = [UICollectionViewLayoutAttributes]()
private var contentWidth: CGFloat = 0
private var height: CGFloat {
get {
return (collectionView?.frame.size.height)!
}
}
override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: height)
}
override func prepare() {
if cache.isEmpty {
let columumnsHeight = (height / CGFloat(numberOfRows))
var yOffset = [CGFloat]()
for column in 0..<numberOfRows {
yOffset.append(CGFloat(column) * (columumnsHeight + 8))
}
var xOffset = [CGFloat](repeating: 0, count: numberOfRows)
var column = 0
for item in 0..<collectionView!.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
let width = delegate.collectionView(collectionView!, widthForPhotoAtIndexPath: indexPath)
let frame = CGRect(x: xOffset[column], y: yOffset[column], width: width, height: columumnsHeight)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = frame
cache.append(attributes)
contentWidth = max(contentWidth, frame.maxX)
xOffset[column] = xOffset[column] + width + 8
column = column >= (numberOfRows - 1) ? 0 : column + 1
//
}
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes] ()
for attributes in cache {
if attributes.frame.intersects(rect) {
layoutAttributes.append(attributes)
}
}
return layoutAttributes
}
}
#
ViewController:
let layout = CameraLayout()
layout.delegate = self
collectionView.collectionViewLayout = layout
extension ViewController: CameraLayoutDelegate {
func collectionView(_ collectionView: UICollectionView, widthForPhotoAtIndexPath indexPath: IndexPath) -> CGFloat {
return .........
}
}
Try to use SKRaggyCollectionViewLayout. Set your collectionView layout class to SKRaggyCollectionViewLayout and connect it:
#property (nonatomic, weak) IBOutlet SKRaggyCollectionViewLayout *layout;
And set the properties of it:
self.layout.numberOfRows = 2;
self.layout.variableFrontierHeight = NO;
https://github.com/tralf/SKRaggyCollectionViewLayout

Resources