s1m0n
s1m0n

Reputation: 7685

Settings UICollectionViewFlowLayout's properties not working when using Auto Layout

I'm trying to set up a UICollectionView with a UICollectionViewFlowLayout with the following requirement: the minimumLineSpacing should always be exactly one-third of the height of the UICollectionView. My initial thought was to override viewDidLayoutSubviews like this:

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    collectionViewFlowLayout.minimumLineSpacing = collectionView.frame.height / 3
    collectionViewFlowLayout.invalidateLayout()
}

Note that I use viewDidLayoutSubviews because I'm planning to use Auto Layout and the frame may depend on some complex constraints. So I can't calculate the frame myself but have to wait until Auto Layout calculated it for me to use in viewDidLayoutSubviews.

I tested this a bit by creating a UICollectionView programmatically (and rotating the simulator to see if the minimumLineSpacing is always correct). It seemed to work just fine.

Then, I switched to Auto Layout. I simply constrained the collection view's top, bottom, leading and trailing space to its superview. After doing so, setting the minimumLineSpacing didn't have the intended effect anymore, it simply didn't change anything about the appearance of the collection view.

The following code nicely demonstrates the issue. As soon as I set useAutoLayout to true, setting the minimumLineSpacing doesn't work anymore.

class DemoViewController: UIViewController, UICollectionViewDataSource {

    var collectionView: UICollectionView!

    var collectionViewFlowLayout: UICollectionViewFlowLayout!

    // MARK: - UIViewController

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionViewFlowLayout = UICollectionViewFlowLayout()
        collectionViewFlowLayout.itemSize = CGSizeMake(100, 100)

        collectionView = UICollectionView(frame: view.frame, collectionViewLayout: collectionViewFlowLayout)
        collectionView.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
        collectionView.dataSource = self

        view.addSubview(collectionView)

        let useAutoLayout = false // Change this to true to test!

        if useAutoLayout {
            collectionView.setTranslatesAutoresizingMaskIntoConstraints(false)

            NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-[collectionView]-|", options: nil, metrics: nil, views: ["collectionView" : collectionView]))
            NSLayoutConstraint.activateConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-[collectionView]-|", options: nil, metrics: nil, views: ["collectionView" : collectionView]))
        } else {
            collectionView.autoresizingMask = .FlexibleHeight | .FlexibleWidth
        }
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        collectionViewFlowLayout.minimumLineSpacing = collectionView.frame.height / 3
        collectionViewFlowLayout.invalidateLayout()
    }

    // MARK: - <UICollectionViewDataSource>

    func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
        return 1
    }

    func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 100
    }

    func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as! UICollectionViewCell

        cell.backgroundColor = UIColor.greenColor()

        return cell
    }

}

Test this code in the Simulator, rotate it and see how setting the minimumLineSpacing doesn't do anything when useAutoLayout is set to true. So my question is: How can I use Auto Layout and still provide a minimumLineSpacing?

Notes
Base SDK is set to iOS 8.4 SDK. Setting other properties like itemSize or minimumInteritemSpacing doesn't work either.

Upvotes: 3

Views: 544

Answers (1)

algal
algal

Reputation: 28094

I've reproduced what you describe. It's very strange.

I think there is something about rotation specifically that causes invalidLayout() to be ignored in this context. Perhaps the problem is that your call to invalidateLayout() occurs at a point where the layout object thinks it has already responded, or is in the process of responding, to the layout invalidation automatically produced by the rotation and consequent bounds change of the collection view. Then your invalidation is ignored, because it comes too late to be coalesced into the automatic one, and too soon to count as a separate invalidation. I'm guessing. I notice that you can even keep incrementing the minimumLineSpacing there, and it will happily march up to infinity without the collection view ever having the wit to do layout again.

But if you set up the view controller so that a shake event triggers the invalidation, then it notices the value.

So you can solve the problem by forcing the invalidation to happen at the next turn of the run loop, thus escaping whatever weird special logic is blocking it during rotation. For instance, if you replace your viewDidLayoutSubviews() with the following:

override func viewDidLayoutSubviews() {
  super.viewDidLayoutSubviews()

  let newValue = collectionView.bounds.height / 3
  dispatch_async(dispatch_get_main_queue()) {
    [weak collectionViewFlowLayout] in
    collectionViewFlowLayout?.minimumLineSpacing = newValue
  }

then it works.

Why should this be necessary? I don't know. I don't think it should be necessary. This feels like a bug in UICollectionView to me, or at least a very unintuitive piece of API.

Upvotes: 1

Related Questions