I have a UICollectionView with a flow layout and each cell is a square. How do I determine the spacing between each cells on each row? I can\'t seem to find the appropriate
There are a few things to consider:
Try changing the minimum spacing in IB, but leave the cursor in that field. Notice that Xcode doesn't immediately mark the document as changed. When you click in a different field, though, Xcode does notice that the document is changed and marks it so in the file navigator. So, be sure to tab or click over to a different field after making a change.
Save your storyboard/xib file after making a change, and be sure to rebuild the app. It's not hard to miss that step, and then you're left scratching your head wondering why your changes didn't seem to have any effect.
UICollectionViewFlowLayout
has a minimumInteritemSpacing
property, which is what you're setting in IB. But the collection's delegate can also have a method to determine the inter-item spacing. That method trump's the layout's property, so if you implement it in your delegate your layout's property won't be used.
Remember that the spacing there is a minimum spacing. The layout will use that number (whether it comes from the property or from the delegate method) as the smallest allowable space, but it may use a larger space if it has space leftover on the line. So if, for example, you set the minimum spacing to 0, you may still see a few pixels between items. If you want more control over exactly how the items are spaced you should probably use a different layout (possibly one of your own creation).
You can do it in two ways.
Firstly, do some modification in layoutAttributesForItem
,
get the current attributes's layout via the previous layoutAttributesForItem(at: IndexPath(item: indexPath.item - 1, section: indexPath.section))?.frame
.
layoutAttributesForItem(at:): This method provides on demand layout information to the collection view. You need to override it and return the layout attributes for the item at the requested indexPath.
Secondly, new some attributes via UICollectionViewLayoutAttributes(forCellWith: indexPath)
,layout them wherever you want.
And the math is a little larger, because it performs the heavy lifting of the layout.
layoutAttributesForElements(in:): In this method you need to return the layout attributes for all the items inside the given rectangle. You return the attributes to the collection view as an array of UICollectionViewLayoutAttributes.
You can check My repo
a swift version base on mokagio:https://github.com/fanpyi/UICollectionViewLeftAlignedLayout-Swift
class UICollectionViewLeftAlignedLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributesToReturn = super.layoutAttributesForElementsInRect(rect)
if let attributesToReturn = attributesToReturn {
for attributes in attributesToReturn {
if attributes.representedElementKind == nil {
let indexpath = attributes.indexPath
if let attr = layoutAttributesForItemAtIndexPath(indexpath) {
attributes.frame = attr.frame
}
}
}
}
return attributesToReturn
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
if let currentItemAttributes = super.layoutAttributesForItemAtIndexPath(indexPath){
let sectionInset = self.evaluatedSectionInsetForItemAtIndex(indexPath.section)
let isFirstItemInSection = indexPath.item == 0;
let layoutWidth = CGRectGetWidth(self.collectionView!.frame) - sectionInset.left - sectionInset.right;
if (isFirstItemInSection) {
currentItemAttributes.leftAlignFrameWithSectionInset(sectionInset)
return currentItemAttributes
}
let previousIndexPath = NSIndexPath(forItem: indexPath.item - 1, inSection: indexPath.section)
let previousFrame = layoutAttributesForItemAtIndexPath(previousIndexPath)?.frame ?? CGRectZero
let previousFrameRightPoint = previousFrame.origin.x + previousFrame.width
let currentFrame = currentItemAttributes.frame;
let strecthedCurrentFrame = CGRectMake(sectionInset.left,
currentFrame.origin.y,
layoutWidth,
currentFrame.size.height)
// if the current frame, once left aligned to the left and stretched to the full collection view
// widht intersects the previous frame then they are on the same line
let isFirstItemInRow = !CGRectIntersectsRect(previousFrame, strecthedCurrentFrame)
if (isFirstItemInRow) {
// make sure the first item on a line is left aligned
currentItemAttributes.leftAlignFrameWithSectionInset(sectionInset)
return currentItemAttributes
}
var frame = currentItemAttributes.frame;
frame.origin.x = previousFrameRightPoint + evaluatedMinimumInteritemSpacingForSectionAtIndex(indexPath.section)
currentItemAttributes.frame = frame;
return currentItemAttributes;
}
return nil
}
func evaluatedMinimumInteritemSpacingForSectionAtIndex(sectionIndex:Int) -> CGFloat {
if let delegate = self.collectionView?.delegate as? UICollectionViewDelegateFlowLayout {
if delegate.respondsToSelector("collectionView:layout:minimumInteritemSpacingForSectionAtIndex:") {
return delegate.collectionView!(self.collectionView!, layout: self, minimumInteritemSpacingForSectionAtIndex: sectionIndex)
}
}
return self.minimumInteritemSpacing
}
func evaluatedSectionInsetForItemAtIndex(index: Int) ->UIEdgeInsets {
if let delegate = self.collectionView?.delegate as? UICollectionViewDelegateFlowLayout {
if delegate.respondsToSelector("collectionView:layout:insetForSectionAtIndex:") {
return delegate.collectionView!(self.collectionView!, layout: self, insetForSectionAtIndex: index)
}
}
return self.sectionInset
}
}
An easy way to left-justify is to modify layoutAttributesForElementsInRect: in your subclass of UICollectionViewFlowLayout:
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray *allLayoutAttributes = [super layoutAttributesForElementsInRect:rect];
CGRect prevFrame = CGRectMake(-FLT_MAX, -FLT_MAX, 0, 0);
for (UICollectionViewLayoutAttributes *layoutAttributes in allLayoutAttributes)
{
//fix blur
CGRect theFrame = CGRectIntegral(layoutAttributes.frame);
//left justify
if(prevFrame.origin.x > -FLT_MAX &&
prevFrame.origin.y >= theFrame.origin.y &&
prevFrame.origin.y <= theFrame.origin.y) //workaround for float == warning
{
theFrame.origin.x = prevFrame.origin.x +
prevFrame.size.width +
EXACT_SPACE_BETWEEN_ITEMS;
}
prevFrame = theFrame;
layoutAttributes.frame = theFrame;
}
return allLayoutAttributes;
}
A cleaner swift version for people interested, based on Chris Wagner's answer:
class AlignLeftFlowLayout: UICollectionViewFlowLayout {
var maximumCellSpacing = CGFloat(9.0)
override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
let attributesToReturn = super.layoutAttributesForElementsInRect(rect) as? [UICollectionViewLayoutAttributes]
for attributes in attributesToReturn ?? [] {
if attributes.representedElementKind == nil {
attributes.frame = self.layoutAttributesForItemAtIndexPath(attributes.indexPath).frame
}
}
return attributesToReturn
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
let curAttributes = super.layoutAttributesForItemAtIndexPath(indexPath)
let sectionInset = (self.collectionView?.collectionViewLayout as UICollectionViewFlowLayout).sectionInset
if indexPath.item == 0 {
let f = curAttributes.frame
curAttributes.frame = CGRectMake(sectionInset.left, f.origin.y, f.size.width, f.size.height)
return curAttributes
}
let prevIndexPath = NSIndexPath(forItem: indexPath.item-1, inSection: indexPath.section)
let prevFrame = self.layoutAttributesForItemAtIndexPath(prevIndexPath).frame
let prevFrameRightPoint = prevFrame.origin.x + prevFrame.size.width + maximumCellSpacing
let curFrame = curAttributes.frame
let stretchedCurFrame = CGRectMake(0, curFrame.origin.y, self.collectionView!.frame.size.width, curFrame.size.height)
if CGRectIntersectsRect(prevFrame, stretchedCurFrame) {
curAttributes.frame = CGRectMake(prevFrameRightPoint, curFrame.origin.y, curFrame.size.width, curFrame.size.height)
} else {
curAttributes.frame = CGRectMake(sectionInset.left, curFrame.origin.y, curFrame.size.width, curFrame.size.height)
}
return curAttributes
}
}
The swift version of Chris solution.
class PazLeftAlignedCollectionViewFlowLayout : UICollectionViewFlowLayout {
var maxCellSpacing = 14.0
override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
if var attributesToReturn = super.layoutAttributesForElementsInRect(rect) as? Array<UICollectionViewLayoutAttributes> {
for attributes in attributesToReturn {
if attributes.representedElementKind == nil {
let indexPath = attributes.indexPath
attributes.frame = self.layoutAttributesForItemAtIndexPath(indexPath).frame;
}
}
return attributesToReturn;
}
return super.layoutAttributesForElementsInRect(rect)
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
let currentItemAttributes = super.layoutAttributesForItemAtIndexPath(indexPath)
if let collectionViewFlowLayout = self.collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
let sectionInset = collectionViewFlowLayout.sectionInset
if (indexPath.item == 0) { // first item of section
var frame = currentItemAttributes.frame;
frame.origin.x = sectionInset.left; // first item of the section should always be left aligned
currentItemAttributes.frame = frame;
return currentItemAttributes;
}
let previousIndexPath = NSIndexPath(forItem:indexPath.item-1, inSection:indexPath.section)
let previousFrame = self.layoutAttributesForItemAtIndexPath(previousIndexPath).frame;
let previousFrameRightPoint = Double(previousFrame.origin.x) + Double(previousFrame.size.width) + self.maxCellSpacing
let currentFrame = currentItemAttributes.frame
var width : CGFloat = 0.0
if let collectionViewWidth = self.collectionView?.frame.size.width {
width = collectionViewWidth
}
let strecthedCurrentFrame = CGRectMake(0,
currentFrame.origin.y,
width,
currentFrame.size.height);
if (!CGRectIntersectsRect(previousFrame, strecthedCurrentFrame)) { // if current item is the first item on the line
// the approach here is to take the current frame, left align it to the edge of the view
// then stretch it the width of the collection view, if it intersects with the previous frame then that means it
// is on the same line, otherwise it is on it's own new line
var frame = currentItemAttributes.frame;
frame.origin.x = sectionInset.left; // first item on the line should always be left aligned
currentItemAttributes.frame = frame;
return currentItemAttributes;
}
var frame = currentItemAttributes.frame;
frame.origin.x = CGFloat(previousFrameRightPoint)
currentItemAttributes.frame = frame;
}
return currentItemAttributes;
}
}
To use it do the following:
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView.collectionViewLayout = self.layout
}
var layout : PazLeftAlignedCollectionViewFlowLayout {
var layout = PazLeftAlignedCollectionViewFlowLayout()
layout.itemSize = CGSizeMake(220.0, 230.0)
layout.minimumLineSpacing = 12.0
return layout
}