3

Overview:

I have a UICollectionView with a simple layout of cards. I use AutoLayout to calculate the sizes of the cells using Self-Sizing cells method.

The problem is that once the UICollectionViewController is shown on the screen, the cells are misaligned. The issue is resolved after I drag the content in the UICollectionView and the cells become aligned again.

Watch Video of the demo of the issue

Note: I'm using a custom UICollectionViewFlowLayout subclass which adds a decoration shadow view behind every UICollectionViewCell. The shadows are always aligned, regardless whether the content is misaligned or not.

Question: Looks like dragging the UICollectionView causes a relayout, so that the items are aligned again. How can I trigger the same action without interaction from the user? Also, rotating the device solves the issue.

Misaligned:

misaligned

Aligned, after dragging:

aligned

Layout code:

import UIKit

final class ShadowCollectionViewFlowLayout: UICollectionViewFlowLayout {
    override init() {
        super.init()
        commonInit()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }

    private func commonInit() {
        minimumLineSpacing = 30
        estimatedItemSize = UICollectionViewFlowLayout.automaticSize
        scrollDirection = .vertical
        registerDecorationViewClass(ShadowCollectionReusableView.self)
    }

    private func registerDecorationViewClass(_ viewClass: UICollectionReusableView.Type) {
        register(viewClass, forDecorationViewOfKind: viewClass.reuseIdentifier)
    }

    override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) {
        if let indexPaths = context.invalidatedItemIndexPaths {
            context.invalidateDecorationElements(ofKind: ShadowCollectionReusableView.reuseIdentifier,
                                                 at: indexPaths)
        }
        super.invalidateLayout(with: context)
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let parent = super.layoutAttributesForElements(in: rect)
        guard let attributes = parent, !attributes.isEmpty else {
            return parent
        }
        let shadowAttributes: [UICollectionViewLayoutAttributes] = attributes.compactMap{ item in
            return self.layoutAttributesForDecorationView(ofKind: ShadowCollectionReusableView.reuseIdentifier,
                                                          at: item.indexPath)
            }
        return attributes + shadowAttributes
    }

    override func layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        let attributes = UICollectionViewLayoutAttributes(forDecorationViewOfKind: ShadowCollectionReusableView.reuseIdentifier,
                                                     with: indexPath)
        guard let itemAttributes = layoutAttributesForItem(at: indexPath) else {return nil}
        let itemFrame = itemAttributes.frame
        let visibleWithPadding = visibleRect.insetBy(dx: 0, dy: -10)
        var frame = itemFrame.intersection(visibleWithPadding)
        if frame == CGRect.null {
            frame = .zero
        }
        attributes.frame = frame
        attributes.zIndex = -1
        return attributes
    }

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

    private var visibleRect: CGRect {
        return CGRect(origin: collectionView?.contentOffset ?? .zero,
                      size: collectionView?.bounds.size ?? .zero)
    }
}

Base cell class:

import UIKit

class SelfSizingCollectionViewCell: UICollectionViewCell {
    private var lessThan: NSLayoutConstraint?
    private var greaterThan: NSLayoutConstraint?

    override func updateConstraints() {
        if lessThan == nil {
            if let window = window {
//                let maxWidth: CGFloat = 556
                let maxWidth: CGFloat = 700
                let width = window.bounds.width - 64

                translatesAutoresizingMaskIntoConstraints = false
                contentView.translatesAutoresizingMaskIntoConstraints = false
                let c = contentView.widthAnchor.constraint(equalToConstant: maxWidth)
                c.priority = .required
                c.isActive = true
                lessThan = c

                if width < maxWidth {
                    let b = contentView.widthAnchor.constraint(greaterThanOrEqualToConstant: width)
                    b.priority = .required
                    b.isActive = true
                    greaterThan = b
                }
            }
        }
        super.updateConstraints()
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        layer.cornerRadius = 10
        clipsToBounds = true
    }
}

Richard Topchii
  • 7,075
  • 8
  • 48
  • 115
  • Apple recommends that you activate self-sizing layout by calling `performBatchUpdates(_:completion:)` during `viewDidLoad`. Does that help? It might or might not. My own experience is that self-sizing cells in a flow layout are a myth (aka "lie") promulgated by Apple; they have _never_ worked for me, as I explain here: https://stackoverflow.com/a/51585910/341994 Still, you might have better luck. – matt Jun 05 '20 at 15:00
  • How exactly should I activate the self sizing layout? What should I implement in `batchUpdates`? Interesting insight, thanks! – Richard Topchii Jun 05 '20 at 15:10
  • 2
    Nothing; you just call it to force re-layout. As I've implied, I don't believe in this trick any more than I believe in self-sizing cells, but if you want to try, go ahead. Personally I think sacrificing a goat and dancing around naked would be more effective. – matt Jun 05 '20 at 15:12
  • So far I wasn't able to get it working with your suggestion, by both, including something in the update block or not including. I also don't believe in self-sizing cells. Agree on your last suggestion, in one of the cases I was able to make self-sizing cells work by sacrificing 3 goats. For this case I've already tried those suggestions and they didn't help, unfortunately. – Richard Topchii Jun 05 '20 at 15:47
  • 1
    It's not _my_ suggestion, it's Apple's suggestion. – matt Jun 05 '20 at 15:52
  • @matt so I've ended up using AsyncDisplayKit (Texture) which did the job flawlessly! – Richard Topchii Jun 25 '20 at 23:01

0 Answers0