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 :
- 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)
- 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.