12

I am trying to understand why Collection View keeps centre aligning just the last cell in a collection.
I have created a simple Flow layout based collection view. I am using the Autolayout flag - which I am not sure is causing this issue.

Whenever I remove a cell from the Collection view - the first few seem to work fine and roll to the left. However when I remove the second last one, then suddenly the last cell changes from being left aligned to being centre aligned. Does anyone have an explanation? It seems strange.

How do I make it so that all the cells will roll to the left and stay aligned to the left.

Edit: here is the view hierachy debugging: https://i.stack.imgur.com/mCGzH.jpg

Here is the view Behaviour

Collectionview centring

I have made a simple github to demo it: https://github.com/grantkemp/CollectionViewIssue

and here is the code:

        var dataforCV = ["short","longer phrase", "Super long Phrase"]

override func viewDidLoad() {
        super.viewDidLoad()
        demoCollectionView.reloadData()
    let layout = UICollectionViewFlowLayout()

    // Bug Description  - > UICollectionViewFlowLayoutAutomaticSize will center Align the layout if it has only a single cell - but it will left align the content if there is more than one cell.

    // I would expect this behaviour to be consistently left aligning the content.

    //How to repeat: If you comment out UICollection​View​Flow​Layout​Automatic​Size then the collection view will show 3 cells being left aligned and it will continue to left align all the content no matter how many cells you remove.

    // But: if you leave turn UICollection​View​Flow​Layout​Automatic​Size on - then it will intially show 3 cells being left aligned, and then as you click to remove each cell they will stay left aligned until the last single cell will suddenly center align in the collection view

    //  see here for the screen recording:https://i.stack.imgur.com/bledY.gif
    //  see here for the view hierachy debuggins screen: https://i.stack.imgur.com/mCGzH.jpg

    layout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize


    // <-- End Bug Description
    demoCollectionView.collectionViewLayout = layout
}

    //MARk: CollectionView

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as? collectionCell
        cell?.text.text  = dataforCV[indexPath.row]

        return cell!

    }

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return dataforCV.count
    }
    //Remove the item from the array if selected
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        dataforCV.remove(at: indexPath.row)
        demoCollectionView.deleteItems(at: [indexPath])
    }
UKDataGeek
  • 6,338
  • 9
  • 46
  • 63

5 Answers5

10

I managed to solve this using the below two steps:

  1. Logged a bug with Apple about the strange behaviour I noticed about the cell behaviour changing when using the UICollectionViewFlowLayoutAutomaticSize
  2. I did a workaround by creating a modified CustomViewFlowLayout ( can't find the original SO/github question where I saw this approach used - I will add it if I find it) I then added the UICollectionViewFlowLayoutAutomaticSize setting - and it's working beautifully.

Sample code:

class MyLeftCustomFlowLayout:UICollectionViewFlowLayout {
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {

        let attributes = super.layoutAttributesForElements(in: rect)

        var leftMargin = sectionInset.left
        var maxY: CGFloat = 2.0

        let horizontalSpacing:CGFloat = 5

        attributes?.forEach { layoutAttribute in
            if layoutAttribute.frame.origin.y >= maxY
                || layoutAttribute.frame.origin.x == sectionInset.left {
                leftMargin = sectionInset.left
            }

            if layoutAttribute.frame.origin.x == sectionInset.left {
                leftMargin = sectionInset.left
            }
            else {
                layoutAttribute.frame.origin.x = leftMargin
            }

            leftMargin += layoutAttribute.frame.width + horizontalSpacing
            maxY = max(layoutAttribute.frame.maxY, maxY)
        }

        return attributes
    }

In the ViewController - you have to add the flow layout to the collection view to make it work:

let layout = MyLeftCustomFlowLayout() 
layout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
myCollectionViewReference.collectionViewLayout = layout

I think the implementation for Collection View can definitely be simplified but I am going to look into it more as I can see how powerful it is.

Sunil Sharma
  • 2,653
  • 1
  • 25
  • 36
UKDataGeek
  • 6,338
  • 9
  • 46
  • 63
  • You can also use ```layout.estimatedItemSize = CGSize(width: 1, height: 1)``` if you are trying to support lower than iOS 10.0. – mn1 Feb 15 '18 at 21:40
5

In Swift 5

Use UICollectionViewFlowLayout to align cell to left like below

let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
self.collectionView.collectionViewLayout = layout
Abdul Karim Khan
  • 4,256
  • 1
  • 26
  • 30
0

When the last cell (only) is showing, try view debugging -> capture view hierarchy (ie. assuming you ran in simulator). My hunch: your cell is growing in size once you have only one left

xaphod
  • 6,392
  • 2
  • 37
  • 45
  • I thought it was that too - but coloured the background colour red of the cell and you can see it stays the same size – UKDataGeek Apr 07 '17 at 05:03
  • Not to flog a dead horse, but did you try capture view hierarchy, and click the lil icon to turn on showing active constraints? Turns up crazy stuff sometimes – xaphod Apr 07 '17 at 05:05
  • Thanks @xaphod - I didn't know about view debugging - thats great. I have attacehed the view debugging for 2 cells and 1 cell - and it looks like there is a new constraint added to centre it - but I have no idea how to resolve it. Is this a bug do you think? http://imgur.com/a/NxidO – UKDataGeek Apr 07 '17 at 06:26
  • I found that if I remove UICollectionViewFlowLayoutAutomaticSize then it works correctly - so I am guessing this is a bug or a "feature" with the new UICollectionViewFlowLayoutAutomaticSize api – UKDataGeek Apr 07 '17 at 06:30
  • If you use automatic sizing then there must be constraints that unambiguously flow all the way from each side of the cell to the other. In your screenshot of view debugging you did not turn on viewing constraints so i cant see them (its a button with a line on the toolbar near middle bottom). Do you have a symbolic breakpoint set for unsatisfiable constraints? – xaphod Apr 07 '17 at 13:21
  • Hi - I added a symbolic breakpoint for constratints but it doesn't seem to trigger. i also can't see any constraints form the automatic sizing. - I think this must be a bug. Any other ideas. – UKDataGeek Apr 07 '17 at 21:23
  • Thanks for the help in debugging @xaphod - I think I found the answer above related to creating my own custom flow layout - and I am pretty sure we have a bug. – UKDataGeek Apr 08 '17 at 10:28
  • Have you checked that your app allows the background refreshing in the Settings? – Ax1le Jan 09 '18 at 11:21
0

If the only requirement difference to a normal UICollectionViewFlowLayout is to ensure that the first item is always left-aligned, it should be enough to simply do just that:

class MyCustomFlowLayout: UICollectionViewFlowLayout {

   override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {

        let attributes = super.layoutAttributesForElements(in: rect)

        attributes?.first?.frame.origin.x = sectionInset.left

        return attributes
    }
}

The downside of this is that it will only work for the first item of the first section. Alternatively, we can use the index path which conveniently comes with the element attributes:

class MyCustomFlowLayout: UICollectionViewFlowLayout {

   override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {

        let attributes = super.layoutAttributesForElements(in: rect)

        attributes?.forEach { (elementAttributes) in
            if elementAttributes.indexPath.item == 0 {
                elementAttributes.frame.origin.x = sectionInset.left
            }
        }

        return attributes
    }
}

It is also possible to target only cells by (in the example above) checking elementAttributes.representedElementCategory, documented here.

Roope
  • 4,469
  • 2
  • 27
  • 51
0

I got this from here. It works for me, setting the estimate size to none

enter image description here

eloka
  • 95
  • 7