Thanks for your response
I implemented all the fixes and suggestions and the issue still persists. I'm testing the app on an iPad A16 iOS 26.4.
This is the implementation:
class PageFlowLayout: UICollectionViewFlowLayout {
override class var layoutAttributesClass: AnyClass {
UICollectionViewLayoutAttributes.self
}
private var calculatedAttributes: [UICollectionViewLayoutAttributes] = []
private var calculatedContentWidth: CGFloat = 0
private var calculatedContentHeight: CGFloat = 0
public weak var delegate: PageFlowLayoutDelegate?
override var collectionViewContentSize: CGSize {
return CGSize(width: self.calculatedContentWidth, height: self.calculatedContentHeight)
}
override init() {
super.init()
self.estimatedItemSize = .zero
self.scrollDirection = .horizontal
self.minimumLineSpacing = 0
self.minimumInteritemSpacing = 0
self.sectionInset = .zero
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepare() {
guard
let collectionView = collectionView,
collectionView.numberOfSections > 0,
calculatedAttributes.isEmpty
else { return }
print("[PageFlowLayout] prepare() — bounds: \(collectionView.bounds.size), currentPage: \(delegate?.currentPage() ?? -1), contentOffset: \(collectionView.contentOffset)")
estimatedItemSize = collectionView.bounds.size
for item in 0..<collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let itemOrigin = CGPoint(x: CGFloat(item) * collectionView.bounds.width, y: 0)
attributes.frame = .init(origin: itemOrigin, size: collectionView.bounds.size)
calculatedAttributes.append(attributes)
}
calculatedContentWidth = collectionView.bounds.width * CGFloat(calculatedAttributes.count)
calculatedContentHeight = collectionView.bounds.size.height
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return calculatedAttributes.compactMap { $0.frame.intersects(rect) ? $0 : nil }
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return calculatedAttributes[indexPath.item]
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
guard let collectionView else { return false }
if newBounds.size != collectionView.bounds.size {
print("[PageFlowLayout] shouldInvalidateLayout — bounds changed: \(collectionView.bounds.size) → \(newBounds.size), currentPage: \(delegate?.currentPage() ?? -1)")
return true
}
if newBounds.size.width > 0 {
let pages = calculatedContentWidth / newBounds.size.width
let arePagesExact = pages.truncatingRemainder(dividingBy: 1) == 0
return !arePagesExact
}
return false
}
override func invalidateLayout() {
calculatedAttributes = []
super.invalidateLayout()
}
override func shouldInvalidateLayout(forPreferredLayoutAttributes preferredAttributes: UICollectionViewLayoutAttributes, withOriginalAttributes originalAttributes: UICollectionViewLayoutAttributes) -> Bool {
return false
}
override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
let context = super.invalidationContext(forBoundsChange: newBounds)
if let collectionView, let currentPage = delegate?.currentPage(), newBounds.width > 0 {
let targetX = CGFloat(currentPage) * newBounds.width
let adjustment = targetX - collectionView.contentOffset.x
print("[PageFlowLayout] invalidationContext — currentPage: \(currentPage), newBounds.width: \(newBounds.width), currentOffset.x: \(collectionView.contentOffset.x), targetX: \(targetX), adjustment: \(adjustment)")
context.contentOffsetAdjustment.x = adjustment
}
return context
}
override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) {
calculatedAttributes = []
print("[PageFlowLayout] invalidateLayout(with:) — offsetAdjustment: \(context.contentOffsetAdjustment)")
super.invalidateLayout(with: context)
}
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
guard let collectionView, let currentPage = delegate?.currentPage() else { return .zero }
let correctedOffset = CGFloat(currentPage) * collectionView.bounds.width
print("[PageFlowLayout] targetContentOffset — proposed: \(proposedContentOffset), currentPage: \(currentPage), bounds.width: \(collectionView.bounds.width), correctedOffset: \(correctedOffset)")
return CGPoint(x: correctedOffset, y: 0)
}
// This function updates the contentOffset in case is wrong
override func finalizeCollectionViewUpdates() {
guard let collectionView, let currentPage = delegate?.currentPage() else { return }
let xPosition = CGFloat(currentPage) * collectionView.bounds.width
print("[PageFlowLayout] finalizeCollectionViewUpdates — currentPage: \(currentPage), expectedX: \(xPosition), actualX: \(collectionView.contentOffset.x)")
if xPosition != collectionView.contentOffset.x {
let offset = CGPoint(x: xPosition, y: 0)
collectionView.setContentOffset(offset, animated: false)
}
}
}
I was testing other approaches to fix the content offset. When I adjust the contentOffset on the layoutSubviews() method in the collection view subclass, it works but not sure if this is the correct approach:
public class FullScreenPageCollectionView: UICollectionView, ... {
private let pageFlowLayout = ...
public override func layoutSubviews() {
let sizeChanged = previousBoundsSize != .zero && bounds.size != previousBoundsSize
previousBoundsSize = bounds.size
super.layoutSubviews()
let expectedOffset = CGFloat(currentPageIndex) * bounds.width
if bounds.width > 0 && abs(contentOffset.x - expectedOffset) > 1 && !isDragging && !isDecelerating && !isScrollingToItem {
contentOffset.x = expectedOffset
}
}
}