Custom UICollectionViewLayout passing layoutattributes for non-existent indexpath - ios

I'm trying to create a custom UICollectionViewFlowLayout where items can be displayed either vertically or horizontally (see below)
Vertical:
|S|S|S|
|1|4|7|
|2|5|8|
|3|6|9|
Horizontal:
|S|1|2|3|
|S|4|5|6|
|S|7|8|9|
However, I cannot seem to get the vertical layout to work properly. The horizontal layout works just fine but every time I want to use the vertical layout it just crashes with the following error:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'UICollectionView received layout attributes for a cell with an index path that does not exist: <NSIndexPath: 0x7ffcf597fa60> {length = 2, path = 1 - 6}'
When I use the same data and force it to use the horizontal layout it works just fine which leads me to believe I'm doing something wrong with the layout attributes.
My custom flow layout class:
import UIKit
class CustomCollectionViewLayout: UICollectionViewFlowLayout {
var horizontalInset = 0.0 as CGFloat
var verticalInset = 0.0 as CGFloat
var minimumItemWidth = 0.0 as CGFloat
var maximumItemWidth = 0.0 as CGFloat
var itemHeight = 0.0 as CGFloat
var itemWidth = 0.0 as CGFloat
var initialItemSizeSet: Bool = false
var _layoutAttributes = Dictionary<String, UICollectionViewLayoutAttributes>()
var _contentSize = CGSize.zero
var transposed: Bool
init(transposed: Bool) {
self.transposed = transposed
super.init()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepare() {
super.prepare()
_layoutAttributes = Dictionary<String, UICollectionViewLayoutAttributes>()
_layoutAttributes.removeAll()
let path = IndexPath(item: 0, section: 0)
let attributes = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, with: path)
let headerHeight = CGFloat(self.itemHeight / 4)
attributes.frame = CGRect(x: 0, y: 0, width: self.collectionView!.frame.size.width, height: headerHeight)
let headerKey = layoutKeyForHeaderAtIndexPath(path)
_layoutAttributes[headerKey] = attributes
let numberOfSections = (self.collectionView!.numberOfSections)-1
var yOffset = headerHeight
var xOffset = self.horizontalInset
var largestNumberOfItems = 0
for section in 0...numberOfSections {
let numberOfItems = self.collectionView!.numberOfItems(inSection: section)
if(numberOfItems > largestNumberOfItems){
largestNumberOfItems = numberOfItems
}
for item in 0...numberOfItems {
let indexPath = IndexPath(item: item, section: section)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
var itemSize = CGSize.zero
//Every cell has a fixed size
if(!initialItemSizeSet){
self.itemWidth = 75
self.itemHeight = 75
initialItemSizeSet = true
}
itemSize.width = itemWidth
itemSize.height = itemHeight
attributes.frame = CGRect(x: xOffset, y: yOffset, width: itemSize.width, height: itemSize.height).integral
print("Created frame with xOffset \(xOffset), yOffset \(yOffset), width \(itemSize.width) and height \(itemSize.height)")
let key = layoutKeyForIndexPath(indexPath)
print("Attached indexPath key: \(key)")
_layoutAttributes[key] = attributes
if transposed {
yOffset += itemSize.height
yOffset += self.verticalInset
}else {
xOffset += itemSize.width
xOffset += self.horizontalInset
}
}
//After going through all items in section, check if current section is not the last section
if !(section == numberOfSections) {
if transposed {
//Increase xOffset for correct horizontal placement
xOffset += self.horizontalInset
xOffset += self.itemWidth
//Reset yOffset for correct spacing between cells
yOffset = self.verticalInset
}else {
//Increase yOffset for correct vertical placement
yOffset += self.verticalInset
yOffset += self.itemHeight
//Reset xOffset for correct spacing between cells
xOffset = self.horizontalInset
}
print("Finished adding items in section \(section), xOffset: \(xOffset), yOffset: \(yOffset)")
}else{
if transposed {
yOffset = self.verticalInset
}else {
xOffset = self.horizontalInset
}
}
}
if transposed {
xOffset += self.itemWidth
}else {
yOffset += self.itemHeight
}
//Calculate size of content based on largest number of items in the sections
print("xOffset: \(xOffset), yOffset: \(yOffset)")
if transposed {
_contentSize = CGSize(width: xOffset + self.horizontalInset, height: (CGFloat(largestNumberOfItems) * (itemHeight + yOffset)))
}else {
_contentSize = CGSize(width: (CGFloat(largestNumberOfItems) * (itemWidth + xOffset)), height: yOffset + self.verticalInset)
}
print("content size :\(_contentSize)")
}
func changeItemSize(_ newWidth: CGFloat, newHeight: CGFloat){
self.itemHeight = newHeight
self.itemWidth = newWidth
}
func changeInset(_ newHorizontalInset: CGFloat, newVerticalInset: CGFloat){
self.horizontalInset = newHorizontalInset
self.verticalInset = newVerticalInset
}
func layoutKeyForIndexPath(_ indexPath : IndexPath) -> String {
return "\(indexPath.section)_\(indexPath.row)"
}
func layoutKeyForHeaderAtIndexPath(_ indexPath : IndexPath) -> String {
return "s_\(indexPath.section)_\(indexPath.row)"
}
override var collectionViewContentSize : CGSize {
return _contentSize
}
override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let headerKey = layoutKeyForIndexPath(indexPath)
return _layoutAttributes[headerKey]
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let key = layoutKeyForIndexPath(indexPath)
return _layoutAttributes[key]
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let predicate = NSPredicate { [unowned self] (evaluatedObject, bindings) -> Bool in
let layoutAttribute = self._layoutAttributes[evaluatedObject as! String]
return rect.intersects(layoutAttribute!.frame)
}
let dict = _layoutAttributes as NSDictionary
let keys = dict.allKeys as NSArray
let matchingKeys = keys.filtered(using: predicate)
return dict.objects(forKeys: matchingKeys, notFoundMarker: NSNull()) as? [UICollectionViewLayoutAttributes]
}
}

So it turns out that the cause of the crash was me forgetting that the number of items returned by the collectionview's numberOfItems(inSection) was not zero-based. I didn't think to look there because the horizontal layout handled this inconsistency fine for some unknown reason.

Related

How to create an image grid/mortar with dynamic sizing while keeping images aspect ratio, without cropping?

I need to dynamically place images in a grid/mortar view while keeping their original aspect ratio. Basically, I am trying to achieve a solution similar to that of Adobe Lightroom.
I originally tried to achieve this by fixing the height, dynamically changing the cell width based on the remaining row space and the image scale. However, because I am using scaleAspectFit the image is scaled, meaning that sometimes some images are cropped.
My guess is that I will have to dynamically play with the height as well, but I do not see how.
The code I am using to do the normalization process is:
var i = 0
while i < sizes.count {
var maxWidth = collectionViewWidth // the width of the UICollectionView
var rowWidth: CGFloat = 0.0
var j = i
while rowWidth < maxWidth, j < sizes.count {
let belowThreshold = sizes[j].width < (maxWidth - rowWidth) * 1.30
let remainsEnough = (maxWidth - rowWidth) / maxWidth > 0.3 && belowThreshold
if belowThreshold || remainsEnough {
rowWidth += sizes[j].width
j += 1
} else { break }
}
let spacing = CGFloat((j - i - 1) * 2)
maxWidth -= spacing
var newRowWidth: CGFloat = 0
for l in i..<j {
let x = (sizes[l].width * maxWidth) / rowWidth
sizes[l].width = x.rounded(to: 3)
newRowWidth += sizes[l].width
}
if newRowWidth >= maxWidth {
let width = sizes[j-1].width - (newRowWidth - maxWidth).rounded(to: 3)
sizes[j-1].width = width.rounded(to: 3)
}
i = j
}
UPDATE 1
Here's a GitHub URL to a sample project with what I currently have: https://github.com/abrahamduran/ios-mortar-view
I wrote starting layout for you. You should verify all the code I posted ofc. Use custom layout like this:
let layout = CustomLayout()
layout.minimumLineSpacing = 4
layout.minimumInteritemSpacing = 4
collectionView.collectionViewLayout = layout
collectionView.backgroundColor = .lightGray
...
// Delegate
func collectionView(_ collectionView: UICollectionView, sizeForPhotoAtIndexPath indexPath: IndexPath) -> CGSize {
return images[indexPath.row].size
}
Code for custom layout.
import UIKit
protocol CustomLayoutDelegate: class {
func collectionView(_ collectionView: UICollectionView, sizeForPhotoAtIndexPath indexPath: IndexPath) -> CGSize
}
class CustomLayout: UICollectionViewFlowLayout {
var preferredHeight: CGFloat = 100 {
didSet {
invalidateLayout()
}
}
fileprivate var cache = [UICollectionViewLayoutAttributes]()
fileprivate var contentSize: CGSize = .zero
override func prepare() {
super.prepare()
cache.removeAll()
guard let collectionView = collectionView,
let delegate = collectionView.delegate as? CustomLayoutDelegate else {
return
}
var sizes: [IndexPath: CGSize] = [:]
let maxRowWidth = collectionView.frame.width - (collectionView.contentInset.left + collectionView.contentInset.right)
var offsetY: CGFloat = 0
var rowIndexes: [IndexPath] = []
var rowWidth: CGFloat = 0
let spacing = minimumInteritemSpacing
let numberOfItems = collectionView.numberOfItems(inSection: 0)
for item in 0..<numberOfItems {
let indexPath = IndexPath(item: item, section: 0)
let size = delegate.collectionView(collectionView, sizeForPhotoAtIndexPath: indexPath)
sizes[indexPath] = size
let aspectRatio = size.width / size.height
let preferredWidth = preferredHeight * aspectRatio
rowWidth += preferredWidth
rowIndexes.append(indexPath)
if rowIndexes.count > 1 {
// Check if we fit row width.
let rowWidthWithSpacing = rowWidth + CGFloat(rowIndexes.count - 1) * spacing
if rowWidthWithSpacing > maxRowWidth {
let previousRowWidthWithSpacing = rowWidthWithSpacing - spacing - preferredWidth
let diff = abs(maxRowWidth - rowWidthWithSpacing)
let previousDiff = abs(maxRowWidth - previousRowWidthWithSpacing)
let scale: CGFloat
let finalRowIndexPaths: [IndexPath]
if previousDiff < diff {
rowWidth -= preferredWidth
rowIndexes.removeLast()
finalRowIndexPaths = rowIndexes
scale = maxRowWidth / rowWidth
rowWidth = preferredWidth
rowIndexes = [indexPath]
} else {
finalRowIndexPaths = rowIndexes
scale = maxRowWidth / rowWidth
rowWidth = 0
rowIndexes = []
}
let finalHeight = preferredHeight * scale
var offsetX: CGFloat = 0
finalRowIndexPaths.forEach {
let size = sizes[$0]!
let scale = finalHeight / size.height
let attributes = UICollectionViewLayoutAttributes(forCellWith: $0)
attributes.frame = CGRect(x: offsetX, y: offsetY, width: size.width * scale, height: size.height * scale).integral
offsetX = attributes.frame.maxX + spacing
cache.append(attributes)
}
offsetY = (cache.last?.frame.maxY ?? 0) + minimumLineSpacing
}
}
if numberOfItems == item + 1 && !rowIndexes.isEmpty {
let finalHeight = preferredHeight
var offsetX: CGFloat = 0
rowIndexes.forEach {
let size = sizes[$0]!
let scale = finalHeight / size.height
let attributes = UICollectionViewLayoutAttributes(forCellWith: $0)
attributes.frame = CGRect(x: offsetX, y: offsetY, width: size.width * scale, height: size.height * scale).integral
offsetX = attributes.frame.maxX + spacing
cache.append(attributes)
}
offsetY = (cache.last?.frame.maxY ?? 0) + minimumLineSpacing
}
contentSize = CGSize(width: collectionView.frame.width, height: cache.last?.frame.maxY ?? 0)
}
}
override var collectionViewContentSize: CGSize {
return contentSize
}
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]
}
}

Center different height CollectionView items

I followed this tutorial: https://www.raywenderlich.com/164608/uicollectionview-custom-layout-tutorial-pinterest-2
and came up with the following code.
My question is now, how I can center the items of my collection view. Every item has a different height. (I have vertical scrolling enabled.)
class DynamicLayout: UICollectionViewFlowLayout {
var delegate: DynamicLayoutDelegate?
var numberOfColumns = 2
fileprivate var cellPadding: CGFloat = 6
fileprivate var contentHeight: CGFloat = 0
var xOffset = 0
fileprivate var contentWidth: CGFloat {
guard let c = collectionView else { return 0 }
return c.bounds.width - (c.contentInset.left + c.contentInset.right)
}
fileprivate var cache = [UICollectionViewLayoutAttributes]()
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
override func prepare() {
guard cache.isEmpty == true, let collectionView = collectionView else { return }
// let columnWidth = contentWidth / CGFloat(numberOfColumns)
let columnWidth = CGFloat(310)
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 cellHeight = delegate?.collectionView(collectionView, heightFor: indexPath)
let height = cellPadding * 2 + cellHeight!
let frame = CGRect(x: xOffset[column], y: yOffset[column], width: columnWidth, height: height)
// let totalCellWidth = CGFloat(310 * numberOfColumns)
// let totalSpacingWidth = cellPadding * CGFloat(numberOfColumns - 1)
// let leftInset = (collectionView.frame.width - CGFloat(totalCellWidth + totalSpacingWidth)) / 2
// let leftInset = collectionView.frame.width - 310 / 2
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] }
}
protocol DynamicLayoutDelegate: class {
func collectionView(_ collectionView:UICollectionView, heightFor indexPath:IndexPath) -> CGFloat
}
The problem is, that I don't want to have empty space below and above smaller cells.
Screenshot of the problem:

UICollectionViewFlowLayout with centered cells

I'm trying to implement a horizontal aligned CollectionView with centered cells and I'm using a custom UICollectionViewFlowLayout. I found several solutions with UICollectionViewDelegateFlowLayout but the delegate is not called, if you use a custom UICollectionViewFlowLayout.
Here is my UICollectionViewFlowLayout. Any ideas what to do?
class MyFlowLayout: UICollectionViewFlowLayout {
var numberOfRows = 1
var cellPadding: CGFloat = 15.0
var availableSize = CGSize.zero
fileprivate var cache = [UICollectionViewLayoutAttributes]()
fileprivate var contentWidth: CGFloat = 0.0
fileprivate var contentHeight: CGFloat {
var collectionViewHeight = collectionView!.bounds.height
if availableSize.height > 0{
collectionViewHeight = availableSize.height
}
let insets = collectionView!.contentInset
return collectionViewHeight - (insets.top + insets.bottom)
}
func reInit(_ newSize: CGSize){
availableSize = newSize
cache.removeAll()
}
override func prepare() {
if cache.isEmpty {
let rowHeight = contentHeight / CGFloat(numberOfRows)
var yOffset = [CGFloat]()
for row in 0 ..< numberOfRows {
yOffset.append(CGFloat(row) * rowHeight )
}
var row = 0
var xOffset = [CGFloat](repeating: 0, count: numberOfRows)
for item in 0 ..< collectionView!.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
let height = rowHeight
let width = CGFloat(height / 1.27)
let frame = CGRect(x: xOffset[row], y: yOffset[row], width: width, height: height)
let insetFrame = frame.insetBy(dx: cellPadding * 2, dy: cellPadding)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = insetFrame
cache.append(attributes)
contentWidth = max(contentWidth, frame.maxX)
xOffset[row] = xOffset[row] + width
row = row >= (numberOfRows - 1) ? 0 : (row + 1)
}
}
}
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
}
}
The layout is connected this way:
collectionView.collectionViewLayout = MyFlowLayout()
collectionView.register(MyCell.self, forCellWithReuseIdentifier: MyCell.identifier)
collectionView.dataSource = self
Prepare in MyFlowLayout is called.

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.

Swift CollectionView Custom Layout Cell spacing

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

Resources