Post

Replies

Boosts

Views

Activity

Reply to Embedded Collection View in SwiftUI offset issue
Thanks for the answer. I implemented the changes but still the same issue is happening, specifically for the 3rd cell (index 2). This is the layout used: 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 } 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 { 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 context.contentOffsetAdjustment.x = adjustment } return context } override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) { calculatedAttributes = [] super.invalidateLayout(with: context) } override func targetContentOffset( forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint ) -> CGPoint { guard let collectionView, collectionView.bounds.width > 0 else { return proposedContentOffset } let pageWidth = collectionView.bounds.width let currentOffset = collectionView.contentOffset.x let currentPage = round(currentOffset / pageWidth) var targetPage: CGFloat if abs(velocity.x) > 0.2 { targetPage = velocity.x > 0 ? currentPage + 1 : currentPage - 1 } else { targetPage = round(proposedContentOffset.x / pageWidth) } let pageCount = CGFloat(collectionView.numberOfItems(inSection: 0)) targetPage = max(0, min(targetPage, pageCount - 1)) return CGPoint(x: targetPage * pageWidth, 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 if xPosition != collectionView.contentOffset.x { let offset = CGPoint(x: xPosition, y: 0) collectionView.setContentOffset(offset, animated: false) } } } I also removed the changes on collection view layoutSubviews() Full implementation is updated here: https://github.com/mrciezas/PagedCollectionViewIssue#
Topic: UI Frameworks SubTopic: General Tags:
Apr ’26
Reply to Embedded Collection View in SwiftUI offset issue
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 } } }
Topic: UI Frameworks SubTopic: General Tags:
Apr ’26
Reply to Embedded Collection View in SwiftUI offset issue
Thanks for the answer. I implemented the changes but still the same issue is happening, specifically for the 3rd cell (index 2). This is the layout used: 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 } 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 { 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 context.contentOffsetAdjustment.x = adjustment } return context } override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) { calculatedAttributes = [] super.invalidateLayout(with: context) } override func targetContentOffset( forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint ) -> CGPoint { guard let collectionView, collectionView.bounds.width > 0 else { return proposedContentOffset } let pageWidth = collectionView.bounds.width let currentOffset = collectionView.contentOffset.x let currentPage = round(currentOffset / pageWidth) var targetPage: CGFloat if abs(velocity.x) > 0.2 { targetPage = velocity.x > 0 ? currentPage + 1 : currentPage - 1 } else { targetPage = round(proposedContentOffset.x / pageWidth) } let pageCount = CGFloat(collectionView.numberOfItems(inSection: 0)) targetPage = max(0, min(targetPage, pageCount - 1)) return CGPoint(x: targetPage * pageWidth, 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 if xPosition != collectionView.contentOffset.x { let offset = CGPoint(x: xPosition, y: 0) collectionView.setContentOffset(offset, animated: false) } } } I also removed the changes on collection view layoutSubviews() Full implementation is updated here: https://github.com/mrciezas/PagedCollectionViewIssue#
Topic: UI Frameworks SubTopic: General Tags:
Replies
Boosts
Views
Activity
Apr ’26
Reply to Embedded Collection View in SwiftUI offset issue
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 } } }
Topic: UI Frameworks SubTopic: General Tags:
Replies
Boosts
Views
Activity
Apr ’26