What I am trying to do:
On Apple's Photo's App, if you rotate device while scroll to an arbitrary offset, same cell that was in the center beforehand would end up in the center after the rotation.
I am trying to achieve the same behavior with UICollectionView. 'indexPathsForVisibleItems' seem to be the way to do it....
What I got so far:
Following code provides smooth operation between rotates, yet the center item calculation seem to be off:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
self.collectionView?.collectionViewLayout.invalidateLayout()
let middleItem = (self.collectionView?.indexPathsForVisibleItems.count / 2) - 1
let indexPath = self.collectionView?.indexPathsForVisibleItems[middleItem]
coordinator.animate(alongsideTransition: { ctx in
self.collectionView?.layoutIfNeeded()
self.collectionView?.scrollToItem(at: indexPath!, at: .centeredVertically, animated: false)
}, completion: { _ in
})
}
What Doesn't work with above code:
- Portrait -> Landscape: Somewhat off from what cell should end up in the center.
- Portrait -> Landscape -> Portrait: Completely off from what offset was in Portrait in the first place.
- Portrait -> Landscape -> Portrait -> Landscape: Way off!
Notes:
- I have to invalidate the collection view layout upon rotation due to cell sizes and spacing having to be recalculated.
- self.collectionView?.layoutIfNeeded() being inside the animation
block, make the transition smooth - May be scrollToItem is being called before the collection view layout is not finalized, leading to incorrect scroll offset?
Alternate Method?
Instead of using 'indexPathsForVisibleItems', use just 'contentOffset'? Calculate the offset after-rotation offset? But how would this be possible when after-rotation contentSize could be different than what's expected do to different cell sizes, cell spacing etc?
This is the implementation of jamesk solutuion in
swift
:In your ViewController:
In UICollectionViewDelegateFlowLayout:
You can do this by implementing the UICollectionViewDelegate method
collectionView(_:targetContentOffsetForProposedContentOffset:)
:Alternatively, if you've already subclassed UICollectionViewLayout, you can implement
targetContentOffset(forProposedContentOffset:)
in your layout subclass, which may be more convenient.In your implementation of that method, compute and return the content offset that would cause the center cell to be positioned in the center of the collection view. If your cell sizes are fixed, it should be a simple matter of undoing the change to the content offset caused by other UI elements (such as the disappearing frames of the status bar and/or navigation bar).
If your cell sizes vary with device orientation:
indexPathForItem(at:)
on your collection view, passing the content offset (plus half of the height of the collection view's visible bounds) as the point.targetContentOffset(forProposedContentOffset:)
methods. Retrieve the layout attributes for the center cell by callinglayoutAttributesForItem(at:)
with the saved index path. The target content offset is the middle of the frame for that item (less half of the height of the collection view's visible bounds).The first step could be implemented in your view controller in
viewWillTransition(to:with:)
or inscrollViewDidScroll()
. It could also be implemented in your layout object inprepareForAnimatedBoundsChange()
, or perhaps ininvalidateLayout(with:)
after checking for a bounds change caused by a change in device orientation. (You would also need to ensure thatshouldInvalidateLayout(forBoundsChange:)
returnstrue
in those circumstances.)You may need to make adjustments for content insets, cell spacing, or other matters specific to your app.