I am following the GitHub source to implement CustomCollectionViewLayout, the problem is occurring when i add space between cells but the functions like minimumLineSpacingForSectionAtIndex and setting edge insets functions are not calling even the delegate has been set. Please let me know how can i set space between each column, since i need space of 15 after first column and space of 10 after other columns.
This is what i have achieved so far:
class DemoGraphicCollectionViewLayout: UICollectionViewLayout {
var numberOfColumns : Int = 0
var itemAttributes : NSMutableArray!
var itemsSize : NSMutableArray!
var contentSize : CGSize!
override func prepare() {
if self.collectionView?.numberOfSections == 0 {
return
}
if (self.itemAttributes != nil && self.itemAttributes.count > 0) {
for section in 0..<self.collectionView!.numberOfSections {
let numberOfItems : Int = self.collectionView!.numberOfItems(inSection: section)
for index in 0..<numberOfItems {
if section != 0 && index != 0 {
continue
}
let attributes : UICollectionViewLayoutAttributes = self.layoutAttributesForItem(at: IndexPath(item: index, section: section))
if section == 0 {
var frame = attributes.frame
frame.origin.y = self.collectionView!.contentOffset.y
attributes.frame = frame
}
if index == 0 {
var frame = attributes.frame
frame.origin.x = self.collectionView!.contentOffset.x
attributes.frame = frame
}
}
}
return
}
if (self.itemsSize == nil || self.itemsSize.count != numberOfColumns) {
self.calculateItemsSize()
}
var column = 0
var xOffset : CGFloat = 0
var yOffset : CGFloat = 0
var contentWidth : CGFloat = 0
var contentHeight : CGFloat = 0
numberOfColumns = (collectionView?.numberOfSections)!
for section in 0..<self.collectionView!.numberOfSections {
let sectionAttributes = NSMutableArray()
for index in 0..<numberOfColumns {
var itemSize = (self.itemsSize[index] as AnyObject).cgSizeValue
let indexPath = IndexPath(item: index, section: section)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = CGRect(x: xOffset, y: yOffset, width: (itemSize?.width)!, height: (itemSize?.height)!).integral
if section == 0 {
itemSize = CGSize(width: 150, height: 30)
}
if section == 0 && index == 0 {
attributes.zIndex = 1024;
} else if section == 0 || index == 0 {
attributes.zIndex = 1023
}
if section == 0 {
var frame = attributes.frame
frame.origin.y = self.collectionView!.contentOffset.y
frame.size.height = 30
attributes.frame = frame
}
if index == 0 {
var frame = attributes.frame
frame.origin.x = self.collectionView!.contentOffset.x
attributes.frame = frame
}
sectionAttributes.add(attributes)
xOffset += (itemSize?.width)!
column += 1
if column == numberOfColumns {
if xOffset > contentWidth {
contentWidth = xOffset
}
column = 0
xOffset = 0
yOffset += (itemSize?.height)!
}
}
if (self.itemAttributes == nil) {
self.itemAttributes = NSMutableArray(capacity: self.collectionView!.numberOfSections)
}
self.itemAttributes .add(sectionAttributes)
}
let attributes : UICollectionViewLayoutAttributes = (self.itemAttributes.lastObject as AnyObject).lastObject as! UICollectionViewLayoutAttributes
contentHeight = attributes.frame.origin.y + attributes.frame.size.height
self.contentSize = CGSize(width: contentWidth, height: CGFloat(0))
}
override var collectionViewContentSize : CGSize {
return self.contentSize
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes {
let aa = self.itemAttributes[indexPath.section] as! NSMutableArray
return aa[indexPath.row] as! UICollectionViewLayoutAttributes
}
override func layoutAttributesForElements(in rect: CGRect) ->[UICollectionViewLayoutAttributes]? {
var attributes = [UICollectionViewLayoutAttributes]()
if let itemAttributes = self.itemAttributes as NSArray as? [[UICollectionViewLayoutAttributes]] {
for section in itemAttributes {
let filteredArray = section.filter {evaluatedObject in
return rect.intersects(evaluatedObject.frame)
}
attributes.append(contentsOf: filteredArray)
}
}
return attributes
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
func sizeForItemWithColumnIndex(_ columnIndex: Int) -> CGSize {
return CGSize(width: 150, height: 130)
}
func calculateItemsSize() {
self.itemsSize = NSMutableArray(capacity: (collectionView?.numberOfSections)!)
for index in 0..<(collectionView?.numberOfSections)! {
self.itemsSize.add(NSValue(cgSize: self.sizeForItemWithColumnIndex(index)))
}
}
}
Try this code. you will only need to replace UIImageView Object with your custom View.
https://github.com/krishnads/KConfigurableCollectionViewDemo
Well, I have tested the code for third party library and i also face the similar issue i.e my delegate methods were not calling. While investigating the issue I found that:
Method minimumInteritemSpacingForSectionAt is a part of UICollectionViewDelegateFlowLayout which is used by the UICollectionViewFlowLayout. The CustomCollectionLayout is a subclass of UICollectionViewLayout and not UICollectionViewFlowLayout. Thats why the delegate methods are not called.
You have an opportunity to adjust the layout in prepare method as mentioned in the source link. Unfortunately i didn't get enough time to implement it but I hope you will find some solution.
Here I found this info
Related
I mean initially there is dummy data then data gets loaded.
I am using collection view for the EPG. I want to load the data dynamically and at the same time I want to set the collection view cell width dynamically.
For example please see the JIOTV app. I am trying to do same thing in my application.
Below is my code of the custom layout.
import UIKit
class CustomCollectionViewLayout: UICollectionViewLayout {
var numberOfColumns = 8
var shouldPinFirstColumn = true
var shouldPinFirstRow = true
var sectionNumber = 0
var itemAttributes = [[UICollectionViewLayoutAttributes]]()
var itemsSize = [CGSize]()
var contentSize: CGSize = .zero
var arr = [String]()
var generalArr = [[String]]()
var durationArr = [String]()
override func prepare() {
guard let collectionView = collectionView else {
return
}
let appDelegate = UIApplication.shared.delegate as! AppDelegate
numberOfColumns = appDelegate.timeArr.count//appDelegate.eventNameArr.count //appDelegate.multipleColumns
// print(numberOfColumns)
if appDelegate.generalArr.count > 0 {
durationArr = appDelegate.generalArr[0]
generalArr = appDelegate.generalArr
if collectionView.numberOfSections == 0 {
return
}
if itemAttributes.count != collectionView.numberOfSections {
generateItemAttributes(collectionView: collectionView)
return
}
for section in 0..<collectionView.numberOfSections {
for item in 0..<collectionView.numberOfItems(inSection: section) {
if section != 0 && item != 0 {
continue
}
let attributes = layoutAttributesForItem(at: IndexPath(item: item, section: section))!
if section == 0 {
var frame = attributes.frame
frame.origin.y = collectionView.contentOffset.y
attributes.frame = frame
}
if item == 0 {
var frame = attributes.frame
frame.origin.x = collectionView.contentOffset.x
attributes.frame = frame
}
}
}
}
}
override var collectionViewContentSize: CGSize
{
return contentSize
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return itemAttributes[indexPath.section][indexPath.row]
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var attributes = [UICollectionViewLayoutAttributes]()
for section in itemAttributes {
let filteredArray = section.filter { obj -> Bool in
return rect.intersects(obj.frame)
}
attributes.append(contentsOf: filteredArray)
}
return attributes
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
func convertHourtoMin(strTime : String) -> Int {
var components: Array = strTime.components(separatedBy: ":")
let hours = Int(components[0]) ?? 0
let minutes = Int(components[1]) ?? 0
let seconds = Int(components[2]) ?? 0
return ((hours * 60) + (minutes) + (seconds / 60))
}
}
// MARK: - Helpers
extension CustomCollectionViewLayout {
func generateItemAttributes(collectionView: UICollectionView) {
if itemsSize.count != numberOfColumns {
calculateItemSizes()
}
var column = 0
var xOffset: CGFloat = 0
var yOffset: CGFloat = 0
var contentWidth: CGFloat = 0
itemAttributes = []
for section in 0..<collectionView.numberOfSections {
var sectionAttributes: [UICollectionViewLayoutAttributes] = []
arr = generalArr[section]
// print("General Array : \(arr)")
// print("General Array count : \(arr.count)")
numberOfColumns = arr.count
durationArr = arr
let appDelegate = UIApplication.shared.delegate as! AppDelegate
for index in 0..<numberOfColumns {
var itemSize = itemsSize[index]
if numberOfColumns == appDelegate.timeArr.count {
itemSize = itemsSize[index]
}
else {
calculateItemSizes()
itemSize = itemsSize[index]
}
let indexPath = IndexPath(item: index, section: section)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = CGRect(x: xOffset, y: yOffset, width: itemSize.width, height: itemSize.height).integral
if section == 0 && index == 0 {
// First cell should be on top
attributes.zIndex = 1024
} else if section == 0 || index == 0 {
// First row/column should be above other cells
attributes.zIndex = 1023
}
// Below code with section == 0 and index till end
/* if section == 0 && 0 < numberOfColumns {
attributes.frame = CGRect(x: xOffset, y: yOffset, width: 100, height: 54).integral
}
*/
if section == 0 {
var frame = attributes.frame
frame.origin.y = collectionView.contentOffset.y
attributes.frame = frame
}
if 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 == numberOfColumns {
if xOffset > contentWidth {
contentWidth = xOffset
}
column = 0
xOffset = 0
yOffset += itemSize.height
}
}
itemAttributes.append(sectionAttributes)
}
if let attributes = itemAttributes.last?.last {
contentSize = CGSize(width: contentWidth, height: attributes.frame.maxY)
}
}
func calculateItemSizes() {
itemsSize = []
let appDelegate = UIApplication.shared.delegate as! AppDelegate
if numberOfColumns == appDelegate.timeArr.count{
for index in 0..<numberOfColumns {
itemsSize.append(sizeForItemWithColumnIndexA(index))
}
}
else {
for index in 0..<numberOfColumns {
itemsSize.append(sizeForItemWithColumnIndex(index))
}
}
}
func sizeForItemWithColumnIndex(_ columnIndex: Int) -> CGSize {
var text: NSString
switch columnIndex {
case 0: return CGSize(width: 80, height: 40) //54
// case 0: return CGSize(width: 106, height: 54)
// case 1:
// text = "MMM-99"
default:
text = "Content"
//return CGSize(width: 100, height: 54)
// for Stringresult in durationArr[columnIndex]
// Below is code to make the cell dynamic
var width:Float = Float(convertHourtoMin(strTime: durationArr[columnIndex]))
var actualWidth:Float = Float((width / 60) * 200) // * 100
actualWidth = actualWidth + actualWidth
// print("Actual Width : \(actualWidth)")
return CGSize(width: Int(actualWidth), height: 40) // 54
//}
}
// let size: CGSize = text.size(withAttributes: [kCTFontAttributeName as NSAttributedStringKey: UIFont.systemFont(ofSize: 14.0)])
// let width: CGFloat = size.width + 16
return CGSize(width: 100, height: 54)
}
// Below method is for time row in EPG VIEW
func sizeForItemWithColumnIndexA(_ columnIndex: Int) -> CGSize {
var text: NSString
switch columnIndex {
case 0: return CGSize(width: 80, height: 25)
// case 0: return CGSize(width: 106, height: 54)
// case 1:
// text = "MMM-99"
default:
text = "Content"
return CGSize(width: 200, height: 25) // originally width : 100
// for Stringresult in durationArr[columnIndex]
// Below is code to make the cell dynamic
var width:Float = Float(convertHourtoMin(strTime: durationArr[columnIndex]))
var actualWidth:Float = Float((width / 60) * 100)
actualWidth = actualWidth + actualWidth
// print("Actual Width : \(actualWidth)")
return CGSize(width: Int(actualWidth), height: 54)
//}
}
// let size: CGSize = text.size(withAttributes: [kCTFontAttributeName as NSAttributedStringKey: UIFont.systemFont(ofSize: 14.0)])
// let width: CGFloat = size.width + 16
return CGSize(width: 100, height: 35)
}
}
Check this out
For an EPG you will basically need to calculate the size of the cell and also it's position for each program for each channel.
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
}
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.
I want to stick 2 column with all section and scroll vertical and horizontal
in my case I have stick 1 column .
let numberOfColumns = 11
var itemAttributes : NSMutableArray!
var itemsSize : NSMutableArray!
var contentSize : CGSize!
override func prepareLayout() {
if self.collectionView?.numberOfSections() == 0 {
return
}
if (self.itemAttributes != nil && self.itemAttributes.count > 0) {
for section in 0..<self.collectionView!.numberOfSections() {
let numberOfItems : Int = self.collectionView!.numberOfItemsInSection(section)
for index in 0..<numberOfItems {
if (section != 0 && index != 0) && (section != 0 && index != 1) {
continue
}
let attributes : UICollectionViewLayoutAttributes = self.layoutAttributesForItemAtIndexPath(NSIndexPath(forItem: index, inSection: section))
if section == 0 {
var frame = attributes.frame
frame.origin.y = self.collectionView!.contentOffset.y
attributes.frame = frame
}
if index == 0 {
var frame = attributes.frame
frame.origin.x = self.collectionView!.contentOffset.x
attributes.frame = frame
}
print ("index : \(index) | section : \(section)")
}
}
return
}
if (self.itemsSize == nil || self.itemsSize.count != numberOfColumns) {
self.calculateItemsSize()
}
var column = 0
var xOffset : CGFloat = 0
var yOffset : CGFloat = 0
var contentWidth : CGFloat = 0
var contentHeight : CGFloat = 0
for section in 0..<self.collectionView!.numberOfSections() {
let sectionAttributes = NSMutableArray()
for index in 0..<numberOfColumns {
let itemSize = self.itemsSize[index].CGSizeValue()
let indexPath = NSIndexPath(forItem: index, inSection: section)
let attributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
attributes.frame = CGRectIntegral(CGRectMake(xOffset, yOffset, itemSize.width, itemSize.height))
if (section == 0 && index == 0) {
attributes.zIndex = 1024;
} else if (section == 0 || index == 0) {
attributes.zIndex = 1023
}
if section == 0 {
var frame = attributes.frame
frame.origin.y = self.collectionView!.contentOffset.y
attributes.frame = frame
}
if index == 0 {
var frame = attributes.frame
frame.origin.x = self.collectionView!.contentOffset.x
attributes.frame = frame
}
print ("index1 : \(index) | section1 : \(section)")
sectionAttributes.addObject(attributes)
xOffset += itemSize.width
column += 1
if column == numberOfColumns {
if xOffset > contentWidth {
contentWidth = xOffset
}
column = 0
xOffset = 0
yOffset += itemSize.height
}
}
if (self.itemAttributes == nil) {
self.itemAttributes = NSMutableArray(capacity: self.collectionView!.numberOfSections())
}
self.itemAttributes .addObject(sectionAttributes)
}
var attributes : UICollectionViewLayoutAttributes = self.itemAttributes.lastObject?.lastObject as! UICollectionViewLayoutAttributes
contentHeight = attributes.frame.origin.y + attributes.frame.size.height
self.contentSize = CGSizeMake(contentWidth, contentHeight)
}
override func collectionViewContentSize() -> CGSize {
return self.contentSize
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
return self.itemAttributes[indexPath.section][indexPath.row] as! UICollectionViewLayoutAttributes
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var attributes = [UICollectionViewLayoutAttributes]()
if self.itemAttributes != nil {
for section in self.itemAttributes {
let filteredArray = section.filteredArrayUsingPredicate(
NSPredicate(block: { (evaluatedObject, bindings) -> Bool in
return CGRectIntersectsRect(rect, evaluatedObject!.frame)
})
) as! [UICollectionViewLayoutAttributes]
attributes.appendContentsOf(filteredArray)
}
}
return attributes
}
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
return true
}
func sizeForItemWithColumnIndex(columnIndex: Int) -> CGSize {
var text : String = ""
switch (columnIndex) {
case 0:
text = "Col 0"
case 1:
text = "Col 1"
case 2:
text = "Col 2"
case 3:
text = "Col 3"
case 4:
text = "Col 4"
case 5:
text = "Col 5"
case 6:
text = "Col 6"
default:
text = "Col 7"
}
let size : CGSize = (text as NSString).sizeWithAttributes([NSFontAttributeName: UIFont.systemFontOfSize(17.0)])
let width : CGFloat = size.width + 40
return CGSizeMake(width, 30)
}
func calculateItemsSize() {
self.itemsSize = NSMutableArray(capacity: numberOfColumns)
for index in 0..<numberOfColumns {
self.itemsSize.addObject(NSValue(CGSize: self.sizeForItemWithColumnIndex(index)))
}
}
enter image description here
In this case I have one cell in stick and I want to add 2 other cell to stick .. how I can do it ??
Problem:
I want to have a grid which scrolls vertically and horizontaly and is updated via CoreData's NSFetchedResultsController
Solution 1: Nested Collection Views in Flow Layout
First collection view is setup with a flow layout and vertical scrolling
Each cell of this collection view is itself a collection view in flow layout which scrolls horizontally.
I then must attach a NSFetchedResultsController to each of these child collection views.
Solution 2 : Implementing a Custom Flow Layout
Put a single collection view in custom layout.
With a single NSFetchedResultsController
Now, 2 would seem much simpler - but creating a custom collection view layout is not something I have done before, and Apple's docs state:
*" *Before you start building custom layouts, consider whether doing so is really necessary. The UICollectionViewFlowLayout class provides
a significant amount of behavior that has already been optimized for
efficiency...."
So my question is - is this a situation in which custom flow layout would really make life easier? If so, has anyone got some Swift examples? I've seen a few in Obj-C, but none in Swift...
Here is my custom CollectionViewLayout in Swift 2.3. You have to implement TwoWayCollectionViewLayoutDatasource to use it.
Hope it can help you.
protocol TwoWayCollectionViewLayoutDatasource: NSObjectProtocol {
func twoWayCollectionViewLayout(layout: TwoWayCollectionViewLayout, colectionView: UICollectionView, widthForColumnIndex index: Int) -> CGFloat
func twoWayCollectionViewLayout(layout: TwoWayCollectionViewLayout, colectionView: UICollectionView, heighForRowIndex index: Int) -> CGFloat
func twoWayCollectionViewLayoutNumberOfFixColumns(layout: TwoWayCollectionViewLayout, colectionView: UICollectionView) -> Int
func twoWayCollectionViewLayoutNumberOfFixRows(layout: TwoWayCollectionViewLayout, colectionView: UICollectionView) -> Int
}
class TwoWayCollectionViewLayout: UICollectionViewLayout {
private var itemAttributes : [[UICollectionViewLayoutAttributes]] = []
private var itemsSize : [[CGSize]] = []
private var contentSize : CGSize = CGSizeZero
weak var dataSource: TwoWayCollectionViewLayoutDatasource!
override func invalidateLayout() {
super.invalidateLayout()
}
override func prepareLayout() {
if self.collectionView?.numberOfSections() == 0 {
return
}
let numberOfColumns = self.collectionView!.numberOfItemsInSection(0)
let numberOfRows = self.collectionView!.numberOfSections()
let numberOfFixColumns = self.dataSource.twoWayCollectionViewLayoutNumberOfFixColumns(self, colectionView: self.collectionView!)
let numberOfFixRows = self.dataSource.twoWayCollectionViewLayoutNumberOfFixRows(self, colectionView: self.collectionView!)
if self.itemAttributes.count > 0 {
for var rowIndex = 0; rowIndex < numberOfRows; rowIndex++ {
for var columnIndex = 0; columnIndex < numberOfColumns; columnIndex++ {
if rowIndex < numberOfFixRows || columnIndex < numberOfFixColumns {
if let attributes = self.layoutAttributesForItemAtIndexPath(NSIndexPath(forItem: columnIndex, inSection: rowIndex)) {
var frame = attributes.frame
if rowIndex < numberOfFixRows {
var yPosition = self.collectionView!.contentOffset.y
for var i = 0; i < rowIndex; i++ {
yPosition += self.itemsSize[i][columnIndex].height
}
frame.origin.y = yPosition
}
if columnIndex < numberOfFixColumns {
var xPosition = self.collectionView!.contentOffset.x
for var i = 0; i < columnIndex; i++ {
xPosition += self.itemsSize[rowIndex][i].width
}
frame.origin.x = xPosition
}
attributes.frame = frame
}
}
}
}
} else {
if (self.itemsSize.count == 0) {
self.calculateItemsSize()
}
var xOffset : CGFloat = 0
var yOffset : CGFloat = 0
var contentWidth : CGFloat = 0
var contentHeight : CGFloat = 0
for var rowIndex = 0; rowIndex < numberOfRows; rowIndex++ {
var rowAttributes: [UICollectionViewLayoutAttributes] = []
for var columnIndex = 0; columnIndex < numberOfColumns; columnIndex++ {
let itemSize = self.itemsSize[rowIndex][columnIndex]
let attributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: NSIndexPath(forItem: columnIndex, inSection: rowIndex))
attributes.frame = CGRectIntegral(CGRectMake(xOffset, yOffset, itemSize.width, itemSize.height))
if columnIndex < numberOfFixColumns && rowIndex < numberOfFixRows {
attributes.zIndex = 1024;
} else if columnIndex < numberOfFixColumns || rowIndex < numberOfFixRows {
attributes.zIndex = 1023
} else {
attributes.zIndex = 1000
}
if columnIndex == 0 {
var frame = attributes.frame
frame.origin.x = self.collectionView!.contentOffset.x
attributes.frame = frame
}
if rowIndex == 0 {
var frame = attributes.frame
frame.origin.y = self.collectionView!.contentOffset.y
attributes.frame = frame
}
rowAttributes.append(attributes)
xOffset += itemSize.width
if columnIndex == numberOfColumns - 1 {
if xOffset > contentWidth {
contentWidth = xOffset
}
xOffset = 0
yOffset += itemSize.height
}
}
self.itemAttributes.append(rowAttributes)
}
if let attributes = self.itemAttributes.last?.last as UICollectionViewLayoutAttributes? {
contentHeight = attributes.frame.origin.y + attributes.frame.size.height
self.contentSize = CGSizeMake(contentWidth, contentHeight)
}
}
}
override func collectionViewContentSize() -> CGSize {
return self.contentSize
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
if self.itemAttributes.count > indexPath.section {
if self.itemAttributes[indexPath.section].count > indexPath.item {
return self.itemAttributes[indexPath.section][indexPath.item]
} else {
print("Dont match row \(indexPath)")
}
} else {
print("Dont match section \(indexPath)")
}
return nil
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var attributes = [UICollectionViewLayoutAttributes]()
for section in self.itemAttributes {
let filteredArray = section.filter({ (item) -> Bool in
return CGRectIntersectsRect(rect, item.frame)
})
attributes.appendContentsOf(filteredArray)
}
return attributes
}
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
return true
}
func calculateItemsSize() {
let numberOfColumns = self.collectionView!.numberOfItemsInSection(0)
let numberOfRows = self.collectionView!.numberOfSections()
self.itemsSize.removeAll()
for var rowIndex = 0; rowIndex < numberOfRows; rowIndex++ {
var rowSize: [CGSize] = []
for var columnIndex = 0; columnIndex < numberOfColumns; columnIndex++ {
let w = self.dataSource.twoWayCollectionViewLayout(self, colectionView: self.collectionView!, widthForColumnIndex: columnIndex)
let h = self.dataSource.twoWayCollectionViewLayout(self, colectionView: self.collectionView!, heighForRowIndex: rowIndex)
rowSize.append(CGSizeMake(w, h))
}
self.itemsSize.append(rowSize)
}
}
}