CollectionView cell does not change backgroundColor - ios

As the title says, I'm incurred in a little trouble using UltraVisualLayout class. When I change the background color to my cell (using UltraVisualLayout as custom layout in the collectionView) cells do not appear to have color, instead if I change to standard flow layout, the cells change their background colors.
I use these lines to color the cells:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "squadraCell", for: indexPath)
switch (indexPath.row % 5)
{
// ho provato anche con cell.contentView.backgroundColor ma non funziona nemmeno così
case 0: cell.contentView.backgroundColor = UIColor.yellow()
case 1: cell.contentView.backgroundColor = UIColor.orange()
case 2: cell.contentView.backgroundColor = UIColor.red()
case 3: cell.contentView.backgroundColor = UIColor.green()
case 4: cell.contentView.backgroundColor = UIColor.blue()
default: break
}
return cell
}
Here my custom layout (https://www.raywenderlich.com/99087/swift-expanding-cells-ios-collection-views)
//
// UltravisualLayout.swift
// RWDevCon
//
// Created by Mic Pringle on 27/02/2015.
// Copyright (c) 2015 Ray Wenderlich. All rights reserved.
//
import UIKit
/* The heights are declared as constants outside of the class so they can be easily referenced elsewhere */
struct UltravisualLayoutConstants
{
struct Cell
{
/* The height of the non-featured cell */
static let standardHeight: CGFloat = 100
/* The height of the first visible cell */
static let featuredHeight: CGFloat = 280
}
}
class UltravisualLayout: UICollectionViewLayout
{
// MARK: Properties and Variables
/* The amount the user needs to scroll before the featured cell changes */
let dragOffset: CGFloat = 180.0
var cache = [UICollectionViewLayoutAttributes]()
/* Returns the item index of the currently featured cell */
var featuredItemIndex: Int
{
get
{
/* Use max to make sure the featureItemIndex is never < 0 */
return max(0, Int(collectionView!.contentOffset.y / dragOffset))
}
}
/* Returns a value between 0 and 1 that represents how close the next cell is to becoming the featured cell */
var nextItemPercentageOffset: CGFloat
{
get
{
return (collectionView!.contentOffset.y / dragOffset) - CGFloat(featuredItemIndex)
}
}
/* Returns the width of the collection view */
var width: CGFloat
{
get
{
return collectionView!.bounds.width
}
}
/* Returns the height of the collection view */
var height: CGFloat
{
get
{
return collectionView!.bounds.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 the content in the collection view */
override func 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.zero
var y: CGFloat = 0
for item in 0..<numberOfItems
{
let indexPath = NSIndexPath(item: item, section: 0)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath as IndexPath)
/* Important because each cell has to slide over the top of the previous one */
attributes.zIndex = item
/* Initially set the height of the cell to the standard height */
var height = standardHeight
if indexPath.item == featuredItemIndex {
/* The featured cell */
let yOffset = standardHeight * nextItemPercentageOffset
y = collectionView!.contentOffset.y - yOffset
height = featuredHeight
} else if indexPath.item == (featuredItemIndex + 1) && indexPath.item != numberOfItems
{
/* The cell directly below the featured cell, which grows as the user scrolls */
let maxY = y + standardHeight
height = standardHeight + max((featuredHeight - standardHeight) * nextItemPercentageOffset, 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 the cache whose frame intersects with the rect passed to the method */
func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]?
{
var layoutAttributes = [UICollectionViewLayoutAttributes]()
for attributes in cache
{
if attributes.frame.intersects(rect)
{
layoutAttributes.append(attributes)
}
}
return layoutAttributes
}
/* Return the content offset of the nearest cell which achieves the nice snapping effect, similar to a paged UIScrollView */
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint
{
let itemIndex = round(proposedContentOffset.y / dragOffset)
let yOffset = itemIndex * dragOffset
return CGPoint(x: 0, y: yOffset)
}
/* Return true so that the layout is continuously invalidated as the user scrolls */
func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool
{
return true
}
}
Here my Xcode project https://www.dropbox.com/sh/7xcu118a9zb7673/AAD6t5zhIgKC5fUUpUTb-yA1a?dl=0
Could you help me?

Related

UICollectionViewFlowLayout with horizontal scroll

I really want to understand what is preventing my collection view to be horizontal?
The code is from online tutorial for expanding Cells when scrolling.
I have set the layout to custom
Have set the class to MyClassFlowLayout in Storyboard
Have set scrollDirection = .horizontal in prepare() method
But the collection view is still vertical.
struct MyLayoutConstants {
struct Cell {
// The height of the non-featured cell
static let standardHeight: CGFloat = 50
// The height of the first visible cell
static let featuredHeight: CGFloat = 150
}
}
import UIKit
class MyClassFlowLayout: UICollectionViewFlowLayout {
// The amount the user needs to scroll before the featured cell changes
let dragOffset: CGFloat = 80.0
var cache: [UICollectionViewLayoutAttributes] = []
// Returns the item index of the currently featured cell
var featuredItemIndex: Int {
// Use max to make sure the featureItemIndex is never < 0
return max(0, Int(collectionView!.contentOffset.y / dragOffset))
}
// Returns a value between 0 and 1 that represents how close the next cell is to becoming the featured cell
var nextItemPercentageOffset: CGFloat {
return (collectionView!.contentOffset.x / dragOffset) - CGFloat(featuredItemIndex)
}
// Returns the width of the collection view
var width: CGFloat {
return collectionView!.bounds.width
}
// Returns the height of the collection view
var height: CGFloat {
return collectionView!.bounds.height
}
// Returns the number of items in the collection view
var numberOfItems: Int {
return collectionView!.numberOfItems(inSection: 0)
}
}
extension MyClassFlowLayout {
// Return the size of all the content in the collection view
override var collectionViewContentSize : CGSize {
let contentHeight = (CGFloat(numberOfItems) * dragOffset) + (height - dragOffset)
return CGSize(width: width, height: contentHeight)
}
override func prepare() {
//-----------------------------------------------------------------
scrollDirection = .horizontal
//----------------------------------------------------------------
cache.removeAll(keepingCapacity: false)
let standardHeight = MyLayoutConstants.Cell.standardHeight
let featuredHeight = MyLayoutConstants.Cell.featuredHeight
var frame = CGRect.zero
var y: CGFloat = 0
for item in 0..<numberOfItems {
let indexPath = IndexPath(item: item, section: 0)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
// Important because each cell has to slide over the top of the previous one
attributes.zIndex = item
// Initially set the height of the cell to the standard height
var height = standardHeight
if indexPath.item == featuredItemIndex {
// The featured cell
let yOffset = standardHeight * nextItemPercentageOffset
y = collectionView!.contentOffset.y - yOffset
height = featuredHeight
} else if indexPath.item == (featuredItemIndex + 1) && indexPath.item != numberOfItems {
// The cell directly below the featured cell, which grows as the user scrolls
let maxY = y + standardHeight
height = standardHeight + max((featuredHeight - standardHeight) * nextItemPercentageOffset, 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 the 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 the content offset of the nearest cell which achieves the nice snapping effect, similar to a paged UIScrollView
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
let itemIndex = round(proposedContentOffset.y / dragOffset)
let yOffset = itemIndex * dragOffset
scrollDirection = .horizontal
return CGPoint(x: 0, y: yOffset)
}
// Return true so that the layout is continuously invalidated as the user scrolls
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
}

How archive like this CollectionView stacked Cell for Overlapping Cell Like Wallet

I want exactly when tap particular cell it will show expand
let layout = UICollectionViewFlowLayout()
layout.minimumLineSpacing = -UIScreen.main.bounds.width/2.08
i want expand cell?
Friends any suggestion and needs helpful for us
Thanks
Usually we use UICollectionViewFlowLayout, once the item's size and spacing is given, the item's frame is settled.
To do
CollectionView stacked Cell for Overlapping Cell Like Wallet
You need to give the frame you want for every item in UICollectionView.
Finishing it by customizing UICollectionViewLayout, and use your own UICollectionViewLayout subclass.
The final effect is as the following image
The basic thing:
To do custom layout is easy:
override public func prepare() ,
to calculate the frames for every item,
And put the frame for item in its container , your custom UICollectionViewLayoutAttributes
override public func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? ,
Retrieves layout information for an item at the specified index path with a corresponding cell.
to assign your prepared custom UICollectionViewLayoutAttributes for item layout
override public func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? ,
Retrieves the layout attributes for all of the cells and views in the specified rectangle.
There is no supplementary views, and decoration views in this situation, just to handle items.
to assign your prepared custom UICollectionViewLayoutAttributes for UICollectionView layout
Key Point:
You have two kinds of layout state.
the initial state, and the unselected state is the same, when no card is selected.
The front card are partly hidden, and the last card is full shown.
The custom UICollectionViewLayoutAttributes, with isExpand to record if there is cell selected.
isExpand is used for an cell added UIPanGestureRecognizer
class CardLayoutAttributes: UICollectionViewLayoutAttributes {
var isExpand = false
override func copy(with zone: NSZone? = nil) -> Any {
let attribute = super.copy(with: zone) as! CardLayoutAttributes
attribute.isExpand = isExpand
return attribute
}
}
The frame calculation of state unselected is easy.
Set the frame of the first item,
then the second ...
y: titleHeight * CGFloat(index) , all OK
fileprivate func setNoSelect(attribute:CardLayoutAttributes) {
guard let collection = collectionView else {
return
}
let noneIdx = Int(collection.contentOffset.y/titleHeight)
if noneIdx < 0 {
return
}
attribute.isExpand = false
let index = attribute.zIndex
var currentFrame = CGRect(x: collection.frame.origin.x, y: titleHeight * CGFloat(index), width: cellSize.width, height: cellSize.height)
if index == noneIdx{
attribute.frame = CGRect(x: currentFrame.origin.x, y: collection.contentOffset.y, width: cellSize.width, height: cellSize.height)
}
else if index <= noneIdx, currentFrame.maxY > collection.contentOffset.y{
currentFrame.origin.y -= (currentFrame.maxY - collection.contentOffset.y )
attribute.frame = currentFrame
}
else {
attribute.frame = currentFrame
}
}
the selected state, to select an item, the item is expanded , others will make room for it.
The logic is to put the selected item in the middle, the y position matters, collection.contentOffset.y + offsetSelected,
The center item's frame is known , then calculate the two sides.
One side is items from (selectedIdx-1) to 0, calculate the frame of item.
The Other side is items from (selectedIdx+1) to the final index , also calculate item's frame.
fileprivate func calculate(for attributes: [CardLayoutAttributes], choose selectedIP: IndexPath) -> [CGRect]{
guard let collection = collectionView else {
return []
}
let noneIdx = Int(collection.contentOffset.y / titleHeight)
if noneIdx < 0 {
return []
}
let x = collection.frame.origin.x
var selectedIdx = 0
for attr in attributes{
if attr.indexPath == selectedIP{
break
}
selectedIdx += 1
}
var frames = [CGRect](repeating: .zero, count: attributes.count)
// Edit here
let offsetSelected: CGFloat = 100
let marginBottomSelected: CGFloat = 10
frames[selectedIdx] = CGRect(x: x, y: collection.contentOffset.y + offsetSelected, width: cellSize.width, height: cellSize.height)
if selectedIdx > 0{
for i in 0...(selectedIdx-1){
frames[selectedIdx - i - 1] = CGRect(x: x, y: frames[selectedIdx].origin.y - titleHeight * CGFloat(i + 1), width: cellSize.width, height: cellSize.height)
}
}
if selectedIdx < (attributes.count - 1){
for i in (selectedIdx + 1)...(attributes.count - 1){
frames[i] = CGRect(x: x, y: frames[selectedIdx].origin.y + marginBottomSelected + titleHeight * CGFloat(i - selectedIdx - 1) + cellSize.height, width: cellSize.width, height: cellSize.height)
}
}
return frames
}
When there is a item selected, you should refresh the custom layout.
To call invalidateLayout(),
Invalidates the current layout and triggers a layout update.
fileprivate var _selectPath: IndexPath? {
didSet {
self.collectionView!.isScrollEnabled = (_selectPath == nil)
}
}
public var selectPath: IndexPath? {
set {
_selectPath = (_selectPath == newValue) ? nil : newValue
self.collectionView?.performBatchUpdates({
self.invalidateLayout()
}, completion: nil)
} get {
return _selectPath
}
}
one more thing, the sample demo in github
Here is the overlapping the layout, minimumLineSpacing negative number
if click the card it should be expanded and scroll down and up card
should not disclose anymoew like wallet
class OverlappedCustomFlowLayout: UICollectionViewFlowLayout {
override func prepare() {
super.prepare()
// This allows us to make intersection and overlapping
// A negative number implies overlapping whereas positive implies space between the adjacent edges of two cells.
minimumLineSpacing = -100
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let layoutAttributes = super.layoutAttributesForElements(in: rect)
for currentLayoutAttributes: UICollectionViewLayoutAttributes in layoutAttributes! {
// zIndex - Specifies the item’s position on the z-axis.
// Unlike a layer's zPosition, changing zIndex allows us to change not only layer position,
// but tapping/UI interaction logic too as it moves the whole item.
currentLayoutAttributes.zIndex = currentLayoutAttributes.indexPath.row + 1
}
return layoutAttributes
}
}
you can try the following idea
The layout part:
var selectedIdx: Int?
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let layoutAttributes = super.layoutAttributesForElements(in: rect)
var originY: CGFloat = 0
let defaultOffsetY: CGFloat = 80
if let idx = selectedIdx{
// expanded layout
for i in 0..<cnt{
// frame the attribute
if i == idx + 1{
// to expand is to make room for the rest items
originY += 400
}
else{
originY += defaultOffsetY
}
}
}
else{
// default layout
for i in 0..<cnt{
// frame the attribute
originY += defaultOffsetY
}
}
return layoutAttributes
}
trigger the expand action
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// edit the state
layout.selectedIdx = indexPath.item
// trigger refreshing the collectionView's layout
collectionView.reloadData()
}
I have tried it's working fine, thanks to everyone
class CardLayout: UICollectionViewLayout {
var contentHeight: CGFloat = 0.0
var cachedAttributes = [UICollectionViewLayoutAttributes]()
var nextIndexPath: Int?
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var collectionViewContentSize: CGSize {
var size = super.collectionViewContentSize
let collection = collectionView!
size.width = collection.bounds.size.width
if let _ = FlowLayoutAttributes.shared.cellIndex{
size.height = contentHeight+UIScreen.main.bounds.width/2+38
}else{
size.height = contentHeight
}
print("Contend",contentHeight)
return size
}
func reloadData(){
self.cachedAttributes = [UICollectionViewLayoutAttributes]()
}
override func prepare() {
cachedAttributes.removeAll()
guard let numberOfItems = collectionView?.numberOfItems(inSection: 0) else {
return
}
for index in 0..<numberOfItems {
let layout = UICollectionViewLayoutAttributes(forCellWith: IndexPath(row: index, section: 0))
layout.frame = frameFor(index: index)
if let indexExpand = FlowLayoutAttributes.shared.cellIndex, indexExpand == index {
self.nextIndexPath = index+1
contentHeight = CGFloat(CGFloat(numberOfItems)*getCardSize())+UIScreen.main.bounds.width/2+38*2
}else{
contentHeight = CGFloat(CGFloat(numberOfItems)*getCardSize())+UIScreen.main.bounds.width/2+38
}
layout.zIndex = index
layout.isHidden = false
cachedAttributes.append(layout)
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes]()
for attributes in cachedAttributes {
if attributes.frame.intersects(rect) {
layoutAttributes.append(cachedAttributes[attributes.indexPath.item])
}
}
return layoutAttributes
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cachedAttributes[indexPath.item]
}
func frameFor(index: Int) -> CGRect {
var frame = CGRect(origin: CGPoint(x: CGFloat(8), y:0), size: CGSize(width: UIScreen.main.bounds.width - CGFloat(8 + 8), height: CGFloat(UIScreen.main.bounds.width/2+38)))
var frameOrigin = frame.origin
if let indexExpand = FlowLayoutAttributes.shared.cellIndex{
if index > 0 {
if indexExpand < index {
let spacesHeight = CGFloat((getCardSize() * CGFloat(index)))+UIScreen.main.bounds.width/2+38-getCardSize()/2
frameOrigin.y = spacesHeight
}else{
frameOrigin.y = CGFloat((getCardSize() * CGFloat(index)))
}
}
}else{
if index > 0 {
frameOrigin.y = CGFloat((getCardSize() * CGFloat(index)))
}
}
frame.origin = frameOrigin
return frame
}
func getCardSize()-> CGFloat{
if UIDevice().userInterfaceIdiom == .phone {
switch UIScreen.main.nativeBounds.height {
case 1136:
print("iPhone 5 or 5S or 5C")
return 45.25
case 1334:
print("iPhone 6/6S/7/8")
return 45.25
case 1920, 2208:
print("iPhone 6+/6S+/7+/8+")
return 46
case 2436:
print("iPhone X/XS/11 Pro")
return 45.25
case 2688:
print("iPhone XS Max/11 Pro Max")
return 46
case 1792:
print("iPhone XR/ 11 ")
return 46
case 2532:
print("iPhone 12/ iPhone 12 Pro")
return 45.50
case 2778:
print("iPhone 12 Pro Max")
return 46.2
default:
return 46.2
}
}else{
return CGFloat(46.2)
}
}
}

adding a custom header to ray wenderlich Pinterest Layout

I used the Pinterest layout file provided from Ray Wenderlich and I implemented the desired layout for cells perfectly, however on a CollectionViewController where I am adding a custom header, I want to add the Pinterest layer below the custom header.
But the Pinterest layer is overriding the custom header and I can no longer find the custom header.
Any suggestion on how I can solve this would be greatly appreciated.
The Pinterest Layout file is below:
import UIKit
protocol CustomLayoutDelegate: class {
// 1. Method to ask the delegate for the height of the image
func collectionView(_ collectionView:UICollectionView, heightForPhotoAtIndexPath indexPath:IndexPath) -> CGFloat
}
class CustomLayout: UICollectionViewFlowLayout {
//1. Pinterest Layout Delegate
weak var delegate: CustomLayoutDelegate!
//2. Configurable properties
fileprivate var numberOfColumns = 2
fileprivate var cellPadding: CGFloat = 5
//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]
}
}
You have to append an attributes for your header before append the first item
Beside that set the yOffset first start from your Header Height
var yOffset = [yourHeaderHeight, yourHeaderHeight]
Add this code between section after the indexPath in section 3
if item == 0 {
let headerAttributes = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, with: indexPath)
headerAttributes.frame = CGRect(x: 0, y: 0, width: yourHeaderWidth, height: yourHeaderHeight)
cache.append(headerAttributes)
}

How to align cell to top by Flow Layout in CollectionView

In this code I am trying to change the size of the first cell of the UICollectionView and the others with the same size but in the first row only one cell is coming out when I want two to come out:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout:
if indexPath.row == 0 {
return CGSize(width: collectionView.frame.width/1.5-2, height: collectionView.frame.width/1.5-2)
}
else {
return CGSize(width: collectionView.frame.width/3.0-3, height: collectionView.frame.width/3.0-3)
}
}
What I really want is this:
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
This is not an exact answer to your question, but maybe it will help you.
Here is a very good tutorial:
https://www.raywenderlich.com/164608/uicollectionview-custom-layout-tutorial-pinterest-2
Item # 6 I handed over for myself:
// 6. Updates the collection view content height
contentHeight = max(contentHeight, frame.maxY)
yOffset[column] = yOffset[column] + height
//Only for 2 columns
if yOffset[1] >= yOffset[0] {
column = 0
} else {
column = 1
}

Custom collection view layout crashes

I've created a custom data grid. It's based on collection view with custom layout. The layout modifies the first section and row attributes making them sticky, so when the user scrolls other rows and sections should go under the sticky ones. The idea for this layout is not mine, I've just adopted it. (I can't give the credits for the real creator, in my research I found so many variations of the layout that I'm not sure which is the original one).
Unfortunately I'm facing a problem with it. I'm getting a crash when scrolling:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'no
UICollectionViewLayoutAttributes instance for
-layoutAttributesForItemAtIndexPath:
Despite the message I think that the real problem is in layoutAttributesForElements method. I've read some threads with a similar problem, but the only working solution is to return all cached attributes, no matter of the passed rectangle. I just don't like quick and dirty solutions like this. I would really appreciate any ideas/solutions you can give me.
The whole project is here. However the most important is the layout so for convenience here it is:
class GridViewLayout: UICollectionViewLayout {
//MARK: - Setup
private var isInitialized: Bool = false
//MARK: - Attributes
var attributesList: [[UICollectionViewLayoutAttributes]] = []
//MARK: - Size
private static let defaultGridViewItemHeight: CGFloat = 47
private static let defaultGridViewItemWidth: CGFloat = 160
static let defaultGridViewRowHeaderWidth: CGFloat = 200
static let defaultGridViewColumnHeaderHeight: CGFloat = 80
static let defaultGridViewItemSize: CGSize =
CGSize(width: defaultGridViewItemWidth, height: defaultGridViewItemHeight)
// This is regular cell size
var itemSize: CGSize = defaultGridViewItemSize
// Row Header Size
var rowHeaderSize: CGSize =
CGSize(width: defaultGridViewRowHeaderWidth, height: defaultGridViewItemHeight)
// Column Header Size
var columnHeaderSize: CGSize =
CGSize(width: defaultGridViewItemWidth, height: defaultGridViewColumnHeaderHeight)
var contentSize : CGSize!
//MARK: - Layout
private var columnsCount: Int = 0
private var rowsCount: Int = 0
private var includesRowHeader: Bool = false
private var includesColumnHeader: Bool = false
override func prepare() {
super.prepare()
rowsCount = collectionView!.numberOfSections
if rowsCount == 0 { return }
columnsCount = collectionView!.numberOfItems(inSection: 0)
// make header row and header column sticky if needed
if self.attributesList.count > 0 {
for section in 0..<rowsCount {
for index in 0..<columnsCount {
if section != 0 && index != 0 {
continue
}
let attributes : UICollectionViewLayoutAttributes =
layoutAttributesForItem(at: IndexPath(forRow: section, inColumn: index))!
if includesColumnHeader && section == 0 {
var frame = attributes.frame
frame.origin.y = collectionView!.contentOffset.y
attributes.frame = frame
}
if includesRowHeader && index == 0 {
var frame = attributes.frame
frame.origin.x = collectionView!.contentOffset.x
attributes.frame = frame
}
}
}
return // no need for futher calculations
}
// Read once from delegate
if !isInitialized {
if let delegate = collectionView!.delegate as? UICollectionViewDelegateGridLayout {
// Calculate Item Sizes
let indexPath = IndexPath(forRow: 0, inColumn: 0)
let _itemSize = delegate.collectionView(collectionView!,
layout: self,
sizeForItemAt: indexPath)
let width = delegate.rowHeaderWidth(in: collectionView!,
layout: self)
let _rowHeaderSize = CGSize(width: width, height: _itemSize.height)
let height = delegate.columnHeaderHeight(in: collectionView!,
layout: self)
let _columnHeaderSize = CGSize(width: _itemSize.width, height: height)
if !__CGSizeEqualToSize(_itemSize, itemSize) {
itemSize = _itemSize
}
if !__CGSizeEqualToSize(_rowHeaderSize, rowHeaderSize) {
rowHeaderSize = _rowHeaderSize
}
if !__CGSizeEqualToSize(_columnHeaderSize, columnHeaderSize) {
columnHeaderSize = _columnHeaderSize
}
// Should enable sticky row and column headers
includesRowHeader = delegate.shouldIncludeHeaderRow(in: collectionView!)
includesColumnHeader = delegate.shouldIncludeHeaderColumn(in: collectionView!)
}
isInitialized = true
}
var column = 0
var xOffset : CGFloat = 0
var yOffset : CGFloat = 0
var contentWidth : CGFloat = 0
var contentHeight : CGFloat = 0
for section in 0..<rowsCount {
var sectionAttributes: [UICollectionViewLayoutAttributes] = []
for index in 0..<columnsCount {
var _itemSize: CGSize = .zero
switch (section, index) {
case (0, 0):
switch (includesRowHeader, includesColumnHeader) {
case (true, true):
_itemSize = CGSize(width: rowHeaderSize.width, height: columnHeaderSize.height)
case (false, true): _itemSize = columnHeaderSize
case (true, false): _itemSize = rowHeaderSize
default: _itemSize = itemSize
}
case (0, _):
if includesColumnHeader {
_itemSize = columnHeaderSize
} else {
_itemSize = itemSize
}
case (_, 0):
if includesRowHeader {
_itemSize = rowHeaderSize
} else {
_itemSize = itemSize
}
default: _itemSize = itemSize
}
let indexPath = IndexPath(forRow: section, inColumn: index)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = CGRect(x: xOffset,
y: yOffset,
width: _itemSize.width,
height: _itemSize.height).integral
// allow others cells to go under
if section == 0 && index == 0 { // top-left cell
attributes.zIndex = 1024
} else if section == 0 || index == 0 {
attributes.zIndex = 1023 // any ohter header cell
}
// sticky part - probably just in case here
if includesColumnHeader && section == 0 {
var frame = attributes.frame
frame.origin.y = collectionView!.contentOffset.y
attributes.frame = frame
}
if includesRowHeader && index == 0 {
var frame = attributes.frame
frame.origin.x = collectionView!.contentOffset.x
attributes.frame = frame
}
sectionAttributes.append(attributes)
xOffset += _itemSize.width
column += 1
if column == columnsCount {
if xOffset > contentWidth {
contentWidth = xOffset
}
column = 0
xOffset = 0
yOffset += _itemSize.height
}
}
attributesList.append(sectionAttributes)
}
let attributes = self.attributesList.last!.last!
contentHeight = attributes.frame.origin.y + attributes.frame.size.height
self.contentSize = CGSize(width: contentWidth,
height: contentHeight)
}
override var collectionViewContentSize: CGSize {
return self.contentSize
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
var curLayoutAttribute: UICollectionViewLayoutAttributes? = nil
if indexPath.section < self.attributesList.count {
let sectionAttributes = self.attributesList[indexPath.section]
if indexPath.row < sectionAttributes.count {
curLayoutAttribute = sectionAttributes[indexPath.row]
}
}
return curLayoutAttribute
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var attributes: [UICollectionViewLayoutAttributes] = []
for section in self.attributesList {
let filteredArray = section.filter({ (evaluatedObject) -> Bool in
return rect.intersects(evaluatedObject.frame)
})
attributes.append(contentsOf: filteredArray)
}
return attributes
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
//MARK: - Moving
override func layoutAttributesForInteractivelyMovingItem(at indexPath: IndexPath,
withTargetPosition position: CGPoint) -> UICollectionViewLayoutAttributes {
guard let dest = super.layoutAttributesForItem(at: indexPath as IndexPath)?.copy() as? UICollectionViewLayoutAttributes else { return UICollectionViewLayoutAttributes() }
dest.transform = CGAffineTransform(scaleX: 1.4, y: 1.4)
dest.alpha = 0.8
dest.center = position
return dest
}
override func invalidationContext(forInteractivelyMovingItems targetIndexPaths: [IndexPath],
withTargetPosition targetPosition: CGPoint,
previousIndexPaths: [IndexPath],
previousPosition: CGPoint) -> UICollectionViewLayoutInvalidationContext {
let context = super.invalidationContext(forInteractivelyMovingItems: targetIndexPaths,
withTargetPosition: targetPosition,
previousIndexPaths: previousIndexPaths,
previousPosition: previousPosition)
collectionView!.dataSource?.collectionView?(collectionView!,
moveItemAt: previousIndexPaths[0],
to: targetIndexPaths[0])
return context
}
}
Implement layoutAttributesForItemAtIndexPath. As per the documentation, "Subclasses must override this method and use it to return layout information for items in the collection view. ".
In my experience this method is normally not called when running in the simulator but can be called on the device. YMMV.

Resources