10

In my app, I have a full-screen paging collection view and each cell needs to be full-screen as well, so the collection view layout's item size needs to be the same size as the view controller's view's bounds size. To do this, in viewDidLayoutSubviews I'm simply setting the item size and it is working as intended. When I present this screen, viewDidLayoutSubviews is called 3 times and each time the view's bounds size is 375.0 x 667.0 for iPhone 7. The cell is sized those same dimensions as expected.

But when using 3D Touch peek and pop to present this screen, the cell is not sized as expected, neither when peeking nor popping. viewDidLayoutSubviews is called 4 times, with these bounds sizes:

--- peek triggered ---
375.0 x 569.524495677234
375.0 x 569.524495677234
375.0 x 720.821325648415
--- pop triggered ---
375.0 x 667.0

When peeking, the collection view is 'full-screen' as expected with a height of 721, and the cell has the expected width of 375, but the cell height is 569 when it should be 721. Upon popping, the collection view height changes to 667 as expected but the cell height is still 569.

I've tried calling collectionView.layoutIfNeeded() after setting the itemSize but the result is the same. Why isn't the collection view updating the cell size here?

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    print("\(view.bounds.width) x \(view.bounds.height)")

    let boundsSize = CGSize(width: Int(view.bounds.width), height: Int(view.bounds.height))
    let flowLayout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout

    if flowLayout.itemSize != boundsSize {
        flowLayout.itemSize = boundsSize

        collectionView.layoutIfNeeded() //this doens't help
    }
}
Zoe
  • 27,060
  • 21
  • 118
  • 148
Jordan H
  • 52,571
  • 37
  • 201
  • 351

5 Answers5

24

Change the collection view's 'Estimate Size' from the inspector to 'None'! None of the above answers worked for me.

Estimate Size

iqra
  • 1,061
  • 1
  • 11
  • 18
17

It seems it's a bug of UICollectionViewFlowLayout, I have to call prepare() before invalidateLayout()

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()


    guard let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else
    {
        return
    }

    layout.itemSize = cellSize
    layout.minimumLineSpacing = space
    layout.prepare()  // <-- call prepare before invalidateLayout
    layout.invalidateLayout()
}
Chen Jiling
  • 519
  • 5
  • 10
1

Try replacing collectionView.layoutIfNeeded() by flowLayout.invalidateLayout()

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    print("\(view.bounds.width) x \(view.bounds.height)")

    let boundsSize = CGSize(width: Int(view.bounds.width), height: Int(view.bounds.height))
    let flowLayout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout

    if flowLayout.itemSize != boundsSize {
        flowLayout.itemSize = boundsSize

        debugPrint("Called")
        flowLayout.invalidateLayout()
    }
}
Zoe
  • 27,060
  • 21
  • 118
  • 148
Reinier Melian
  • 20,519
  • 3
  • 38
  • 55
0

You can use collectionView(_:layout:sizeForItemAt:) along with viewDidLayoutSubviews.

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    print("\(view.bounds.width) x \(view.bounds.height)")

    let boundsSize = CGSize(width: Int(view.bounds.width), height: Int(view.bounds.height))

    if !boundsSize.equalTo(self. collectionViewItemSize) {
        collectionView.invalidateLayout()
    }
}

var collectionViewItemSize:CGSize = .zero

func collectionView(_ collectionView: UICollectionView,
                    layout collectionViewLayout: UICollectionViewLayout,
                    sizeForItemAt indexPath: IndexPath) -> CGSize {
    self.collectionViewItemSize = CGSize(width: Int(view.bounds.width), height: Int(view.bounds.height));
    return self.collectiViewItemSize;
}
Puneet Sharma
  • 9,369
  • 1
  • 27
  • 33
  • This gives the same result. So it first calls `viewDidLayoutSubviews` then `sizeForItemAt` repeatedly and returns the expected size. Then did layout is called again with that same size, it's invalidated and calls size for item repeatedly again (if I take out the check to be sure the size actually changed), then did layout is called the third and fourth time but it's not invalided it seems because it doesn't call `sizeForItemAt ` again. – Jordan H Jul 30 '17 at 00:37
  • @Joey: Strange. Call setLayout immediately after calling invalidateLayout. That should definitely trigger sizeForItemAt method. – Puneet Sharma Jul 30 '17 at 03:59
  • 2
    Strange indeed. I finally fixed it by `invalidateLayout()` then `layoutIfNeeded()` then `reloadData()`. Shouldn't be necessary, so weird. – Jordan H Jul 30 '17 at 04:53
0

I ended up changing the itemSize in viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) and passing the size to a method that'll do the math based on the future size of the view.

Yariv Nissim
  • 13,273
  • 1
  • 38
  • 44