I am using the following code in order to have a UICollectionView with variable height for various cells:
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return totalItems
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
totalItems -= 1
collectionView.deleteItems(at: [indexPath])
collectionView.collectionViewLayout.invalidateLayout()
}
}
extension InviteViewController : PinterestLayoutDelegate {
// 1. Returns the cell height
func collectionView(_ collectionView: UICollectionView, heightForPhotoAtIndexPath indexPath:IndexPath) -> CGFloat {
print("have: ", indexPath.item)
if(indexPath.item % 3 == 0) {
return 150
} else if(indexPath.item % 3 == 1) {
return 200
} else {
return 250
}
}
}
However I see that the layout doesn't update after the deletion of the UICollectionViewCell and I get errors as:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'UICollectionView received layout attributes for a cell with an index path that does not exist: {length = 2, path = 0 - 7}'
I am stuck and unable to understand why this happens and how to solve this. Please help me find a solution for the same. Thank you.
In case you want to have a look at the layout file I used, please have a look here:
import UIKit
protocol PinterestLayoutDelegate: class {
// 1. Method to ask the delegate for the height of the image
func collectionView(_ collectionView:UICollectionView, heightForPhotoAtIndexPath indexPath:IndexPath) -> CGFloat
}
class PinterestLayout: UICollectionViewLayout {
//1. Pinterest Layout Delegate
weak var delegate: PinterestLayoutDelegate!
//2. Configurable properties
fileprivate var numberOfColumns = 2
fileprivate var cellPadding: CGFloat = 3
//3. Array to keep a cache of attributes.
fileprivate var cache = [UICollectionViewLayoutAttributes]()
//4. Content height and size
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() {
// 1. Only calculate once
guard cache.isEmpty == true, let collectionView = collectionView else {
return
}
// 2. Pre-Calculates the X Offset for every column and adds an array to increment the currently max Y Offset for each column
let columnWidth = contentWidth / CGFloat(numberOfColumns)
var xOffset = [CGFloat]()
for column in 0 ..< numberOfColumns {
xOffset.append(CGFloat(column) * columnWidth)
}
var column = 0
var yOffset = [CGFloat](repeating: 0, count: numberOfColumns)
// 3. Iterates through the list of items in the first section
for item in 0 ..< collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
// 4. Asks the delegate for the height of the picture and the annotation and calculates the cell frame.
let photoHeight = delegate.collectionView(collectionView, heightForPhotoAtIndexPath: indexPath)
let height = cellPadding * 2 + photoHeight
let frame = CGRect(x: xOffset[column], y: yOffset[column], width: columnWidth, height: height)
let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
// 5. Creates an UICollectionViewLayoutItem with the frame and add it to the cache
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = insetFrame
cache.append(attributes)
// 6. Updates the collection view content height
contentHeight = max(contentHeight, frame.maxY)
yOffset[column] = yOffset[column] + height
column = column < (numberOfColumns - 1) ? (column + 1) : 0
}
}
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]
}
}
I followed: https://www.raywenderlich.com/392-uicollectionview-custom-layout-tutorial-pinterest
override func prepare() {
// 1. Only calculate once
guard cache.isEmpty == true, let collectionView = collectionView else {
return
}
In this part delete guard cache.isEmpty == true,. It works.
extension CategoryPageVC: PinterestLayoutDelegate {
func collectionView(_ collectionView: UICollectionView,
heightForPhotoAtIndexPath indexPath:IndexPath) -> CGFloat {
let rate = self.categoryData[indexPath.item].image.size.width / self.collectionView.frame.size.width * 2
return self.categoryData[indexPath.item].image.size.height / rate
}
}
Related
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
i have a small question about a custom layout class and i need to connect it to my collection view in code not by the story board because i create the whole project by code . any help, please ?
my main class
import UIKit
import Firebase
class UserProfileController: UICollectionViewController {
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.backgroundColor = .white
//fetchUser()
collectionView?.dataSource = self
collectionView?.delegate = self
collectionView?.register(TestCell.self, forCellWithReuseIdentifier: mainCellId)
}
fileprivate let mainCellId = "mainCellId"
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: mainCellId, for: indexPath) as! TestCell
return cell
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 15
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
}
the custom layout class which need to connect it to the collection view in code not story board
import UIKit
struct UltraVisualLayoutConstants {
struct Cell {
static let standardHeight: CGFloat = 100
static let featuredHeight: CGFloat = 280
// static let standardWidth: CGFloat = 100
// static let featuredWidth: CGFloat = 280
}
}
class UltraVisualLayout: UICollectionViewLayout {
// amount which users need to scroll before featured cell changes\
let dragOffset: CGFloat = 180.0
var cache = [UICollectionViewLayoutAttributes]()
//return item index of current featured cell
var featuredItemIndex: Int {
get {
//use max to ensure that featureditemindex never be < 0
return max(0 , Int(collectionView!.contentOffset.y / dragOffset))
}
}
// returns value between 0 and 1 to represent how close the next cell becomes the featured cell
var nextItemPercentegeOffset: CGFloat {
get {
return (collectionView!.contentOffset.y / dragOffset) - CGFloat(featuredItemIndex)
}
}
// return the width of collection view
var width: CGFloat {
get {
guard let width = collectionView?.bounds.width else {return 0}
return width
}
}
// return the height of collection view
var height: CGFloat {
get {
guard let height = collectionView?.bounds.height else {return 0}
return height
}
}
//returns the number of items in the collection view
var numberOfItems: Int {
get {
return collectionView!.numberOfItems(inSection: 0)
}
}
// MARK: UICollectionViewLayout
// return the size of all content in collection view
override var collectionViewContentSize: CGSize {
let contentHeight = (CGFloat(numberOfItems) * dragOffset) + (height - dragOffset)
return CGSize(width: width, height: contentHeight)
}
override func prepare() {
cache.removeAll(keepingCapacity: false)
let standardHeight = UltraVisualLayoutConstants.Cell.standardHeight
let featuredHeight = UltraVisualLayoutConstants.Cell.featuredHeight
var frame = CGRect(x: 0, y: 0, width: 0, height: 0)
var y: CGFloat = 0
for item in 0..<numberOfItems {
let indexPath = NSIndexPath(item: item, section: 0)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath as IndexPath)
attributes.zIndex = item
var height = standardHeight
if indexPath.item == featuredItemIndex {
let yOffset = standardHeight * nextItemPercentegeOffset
y = collectionView!.contentOffset.y - yOffset
height = featuredHeight
} else if indexPath.item == (featuredItemIndex + 1) && indexPath.item != numberOfItems {
let maxY = y + standardHeight
height = standardHeight + max((featuredHeight - standardHeight) * nextItemPercentegeOffset, 0)
y = maxY - height
}
frame = CGRect(x: 0, y: y, width: width, height: height)
attributes.frame = frame
cache.append(attributes)
y = frame.maxY
}
}
// return all attributes in cache whose frame intersects with the rect passed to the method
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes]()
for attributes in cache {
if attributes.frame.intersects(rect) {
layoutAttributes.append(attributes)
}
}
return layoutAttributes
}
// return true so that layout is continuously invalidated as the user scrolls
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
You cannot set the layout programmatically if you don't have an initializer. From the docs:
layout
The layout object to use for organizing items. The collection
view stores a strong reference to the specified object. Must not be
nil.
Since you seem to be using the storyboard, you'll have to set it there.
Once you create you layout class in code, it will show up when you click you collection view on the storyboard. The default layout will be set to Flow, when you change it to Custom, a new class field will show up. When you click that, it will list your layout class. You can choose it and set it from there.
However if you are initializing your storyboard programmatically, then you just need to pass it as a parameter to your initializer.
var collectionView = UICollectionView(frame: yourFrame, collectionViewLayout: customCollectionViewLayout) // pass your custom collection view instance
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.
I have ran into a problem during UICollectionView custom layout implementation.
The thing is that I need to calculate collection view cell's height in custom layout's prepare(). To do so I have:
func heightForItem(_ collectionView: UICollectionView, at indexPath: IndexPath) -> CGFloat
method in custom layout's delegate protocol which is implemented in my view controller.
However this method is called before cell is dequeued and actually has any data based on which I could calculate it's height. Thus if cell's content exceeds it's initial bounds - I can't see part of content.
Has anyone encountered the same problem with custom layout? How did you solve it?
Protocol for custom layout:
protocol CustomLayoutDelegateProtocol: class {
func numberOfSectionsInRow() -> Int
func indecesOfSectionsInRow() -> [Int]
func minimumInteritemSpace() -> CGFloat
func heightForItem(_ collectionView: UICollectionView, at indexPath: IndexPath) -> CGFloat
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets
}
Custom layout itself:
class CustomLayoutClass: UICollectionViewLayout {
weak var delegate: CustomLayoutDelegateProtocol? {
didSet {
setupLayout()
}
}
private var cache = [UICollectionViewLayoutAttributes]()
private var contentHeight: CGFloat = 0.0
private var contentWidth: CGFloat {
guard let collectionView = collectionView else { return 0 }
return collectionView.bounds.width
}
private var interitemSpace: CGFloat?
private var numberOfColumns: Int?
private var columnedSections: [Int]?
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
private func setupLayout() {
numberOfColumns = delegate?.numberOfSectionsInRow()
columnedSections = delegate?.indecesOfSectionsInRow()
interitemSpace = delegate?.minimumInteritemSpace()
}
override func invalidateLayout() {
cache.removeAll()
super.invalidateLayout()
}
override func prepare() {
if cache.isEmpty {
guard let collectionView = collectionView,
let numberOfColumns = numberOfColumns,
let columnedSections = columnedSections,
let interitemSpace = interitemSpace else { return }
let columnWidth = (contentWidth / CGFloat(numberOfColumns))
var xOffset = [CGFloat]()
for column in 0..<numberOfColumns {
var interitemSpace = interitemSpace
if column == 0 { interitemSpace = 0 }
xOffset.append(CGFloat(column) * columnWidth + interitemSpace)
}
var yOffset: CGFloat = 0.0
for section in 0..<collectionView.numberOfSections {
for item in 0..<collectionView.numberOfItems(inSection: section) {
let indexPath = IndexPath(item: item, section: section)
guard let sectionInsets = delegate?.collectionView(collectionView, layout: self, insetForSectionAt: indexPath.section),
let height = delegate?.heightForItem(collectionView, at: indexPath) else { continue }
let width = columnedSections.contains(section) ? columnWidth : contentWidth
let xOffsetIdx = columnedSections.contains(section) ? columnedSections.index(of: section)! % numberOfColumns : 0
yOffset += sectionInsets.top
let frame = CGRect(x: xOffset[xOffsetIdx], y: yOffset, width: width, height: height)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = frame
cache.append(attributes)
contentHeight = max(contentHeight, frame.maxY)
let isLastInRow = (columnedSections.contains(section) && columnedSections.index(of: section)! % numberOfColumns == (numberOfColumns-1))
let isNotColumnedSection = !columnedSections.contains(section)
if isLastInRow || isNotColumnedSection {
yOffset += height + sectionInsets.bottom
}
}
}
}
}
override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: contentHeight)
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes]()
for attributes in cache {
if attributes.frame.intersects(rect) {
layoutAttributes.append(attributes)
}
}
return layoutAttributes
}
}
And implementation for protocol (from view controller):
extension ViewController: CustomLayoutDelegateProtocol {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
guard let sectionType = InvoiceSectionIndexType(rawValue: section) else { return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) }
switch sectionType {
case .receiver:
return UIEdgeInsets(top: Constants.bigLineSpace, left: 0, bottom: Constants.bigLineSpace, right: 0)
default:
return UIEdgeInsets(top: 0, left: 0, bottom: Constants.commonLineSpace, right: 0)
}
}
func numberOfSectionsInRow() -> Int {
return Constants.numberOfSectionsInRow
}
func indecesOfSectionsInRow() -> [Int] {
return [0, 1]
}
func minimumInteritemSpace() -> CGFloat {
return Constants.interitemSpace
}
func heightForItem(_ collectionView: UICollectionView, at indexPath: IndexPath) -> CGFloat {
guard let sectionType = InvoiceSectionIndexType(rawValue: indexPath.section) else { return 0 }
switch sectionType {
case .next:
return Constants.nextButtonHeight
default:
return Constants.estimatedRowHeight
}
}
}
I resolved this issue on my own and the solution is quite simple though not very obvious.
Since we have indexPath for concrete cell we can find our content (in my case content is simple text which is put into a label). Then we can create label (which we will not display since it's needed only for calculations) with width (in my case I know width for cells) and height which is CGFloat.greatestFiniteMagnitude. We then apply sizeToFit() to our label and now we have label with appropriate height. The only thing we should do now - apply this new height to our cell.
Sample code:
func collectionView(_ collectionView: UICollectionView, heightForPhotoAtIndexPath indexPath: IndexPath, withWidth width: CGFloat) -> CGFloat {
// ask your DataSource for piece of data with indexPath
let testLabel = UILabel(frame: CGRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude))
testLabel.text = // your text
testLabel.sizeToFit()
if testLabel.bounds.height > Constants.defaultContentLabelHeight {
return Constants.estimatedRowHeight + (testLabel.bounds.height - Constants.defaultContentLabelHeight)
} else {
return Constants.estimatedRowHeight
}
}
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