Wrong frame from layoutAttributesForItem when using UICollectionViewFlowLayout with an UIDynamicAnimator

456 Views Asked by At

So I implemented an UICollectionView with a custom UICollectionViewFlowLayout containing a UIDynamicAnimator for animating my cells upon scrolling. I used the 2013 WWDC reference to replicate the Message bounce.

Everything is working fine, except that I noticed a weird cut in one side of my rounded views added in my cell. See screenshots below :

weird cut

  1. Is a screenshot of a cell when my FlowLayout is initialized with an UIDynamicAnimator. (Nothing fancy, I'm just showing one item which I added to a UICollisionBehavior linked to my animator, see code below)
  2. Is the same cell when using a simple FlowLayout without animator

If we pay close attention, we can notice that n°1 is missing a one-pixel vertical line on the red side, and that green side has one more.

This result in a cut effect on every subviews contained in my cell (no matter if its a view, an image etc.)

So I investigated to understand what was causing this, and I found that from the second pass into the method layoutAttributesForElements(in rect: CGRect) the returned x position was wrong.

My method sizeForItemAt() is returning a classic CGSize(width: collectionView.bounds.width, height: 100), but dynamicAnimator.items(in: rect) is returning a frame equal to CGRect(0.1666666666666572, 0.0, 375.0, 100.0) for my cell.

This x position is supposed to be 0 as I'm not applying any transform myself.

0.1666666666666572 being equal to 1/6, this looks like a float-precision issue.

Does anyone has an idea of what is causing this, and how to solve it ?

import Foundation
import UIKit

// Minimal implementation of https://developer.apple.com/videos/play/wwdc2013/217/ to reproduce 0.16667 error

class MinimalWWDCFlowLayout: UICollectionViewFlowLayout {
    lazy var behavior: UICollisionBehavior = .init()
    lazy var dynamicAnimator: UIDynamicAnimator = {
        let res = UIDynamicAnimator(collectionViewLayout: self)
        res.addBehavior(behavior)
        return res
    }()

    override func prepare() {
        super.prepare()

        guard let items = super.layoutAttributesForElements(in: .init(origin: .zero, size: collectionViewContentSize)),
              let firstItem = items.first else {
            return
        }
        guard behavior.items.isEmpty else {
            return
        }
        behavior.addItem(firstItem) // This create a debug log when called twice (no error, just a log)
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let items = dynamicAnimator.items(in: rect) as? [UICollectionViewLayoutAttributes]

        guard let firstItem = items?.first else { return nil }
        print("️ item frame: \(firstItem.frame))") // This is returning frame.x = 0.1666..67. Calling super.layoutAttributesForElements(in:) instead is returning 0 as expected.

        return items
    }

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        return dynamicAnimator.layoutAttributesForCell(at: indexPath)
    }

    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        return false
    }
}

Note: this seems to happen only on devices with 3x res. Running this code on an iPhone 11 works fine, but result in the described bug on an iPhone 11 Pro.

0

There are 0 best solutions below