Post

Replies

Boosts

Views

Activity

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:
3w
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:
3w
Reply to uploaded binary nowhere to be found
Same here. Status according to this looks like App Store Connect is having issues.
Replies
Boosts
Views
Activity
Nov ’22
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
3w
Reply to Embedded Collection View in SwiftUI offset issue
This is a GitHub repo with a full demo project: https://github.com/mrciezas/PagedCollectionViewIssue#
Topic: UI Frameworks SubTopic: General Tags:
Replies
Boosts
Views
Activity
3w
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
3w