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:
Aligned, after dragging:
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
}
}