1

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.

Cerise
  • 383
  • 3
  • 16

0 Answers0