Keeping the contentOffset in a UICollectionView while rotating Interface Orientation
Solution 1
You can either do this in the view controller:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
guard let collectionView = collectionView else { return }
let offset = collectionView.contentOffset
let width = collectionView.bounds.size.width
let index = round(offset.x / width)
let newOffset = CGPoint(x: index * size.width, y: offset.y)
coordinator.animate(alongsideTransition: { (context) in
collectionView.reloadData()
collectionView.setContentOffset(newOffset, animated: false)
}, completion: nil)
}
Or in the layout itself: https://stackoverflow.com/a/54868999/308315
Solution 2
Solution 1, "just snap"
If what you need is only to ensure that the contentOffset ends in a right position, you can create a subclass of UICollectionViewLayout and implement targetContentOffsetForProposedContentOffset: method. For example you could do something like this to calculate the page:
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset
{
NSInteger page = ceil(proposedContentOffset.x / [self.collectionView frame].size.width);
return CGPointMake(page * [self.collectionView frame].size.width, 0);
}
But the problem that you'll face is that the animation for that transition is extremely weird. What I'm doing on my case (which is almost the same as yours) is:
Solution 2, "smooth animation"
1) First I set the cell size, which can be managed by collectionView:layout:sizeForItemAtIndexPath: delegate method as follows:
- (CGSize)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout *)collectionViewLayout
sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
return [self.view bounds].size;
}
Note that [self.view bounds] will change according to the device rotation.
2) When the device is about to rotate, I'm adding an imageView on top of the collection view with all resizing masks. This view will actually hide the collectionView weirdness (because it is on top of it) and since the willRotatoToInterfaceOrientation: method is called inside an animation block it will rotate accordingly. I'm also keeping the next contentOffset according to the shown indexPath so I can fix the contentOffset once the rotation is done:
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
duration:(NSTimeInterval)duration
{
// Gets the first (and only) visible cell.
NSIndexPath *indexPath = [[self.collectionView indexPathsForVisibleItems] firstObject];
KSPhotoViewCell *cell = (id)[self.collectionView cellForItemAtIndexPath:indexPath];
// Creates a temporary imageView that will occupy the full screen and rotate.
UIImageView *imageView = [[UIImageView alloc] initWithImage:[[cell imageView] image]];
[imageView setFrame:[self.view bounds]];
[imageView setTag:kTemporaryImageTag];
[imageView setBackgroundColor:[UIColor blackColor]];
[imageView setContentMode:[[cell imageView] contentMode]];
[imageView setAutoresizingMask:0xff];
[self.view insertSubview:imageView aboveSubview:self.collectionView];
// Invalidate layout and calculate (next) contentOffset.
contentOffsetAfterRotation = CGPointMake(indexPath.item * [self.view bounds].size.height, 0);
[[self.collectionView collectionViewLayout] invalidateLayout];
}
Note that my subclass of UICollectionViewCell has a public imageView property.
3) Finally, the last step is to "snap" the content offset to a valid page and remove the temporary imageview.
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
[self.collectionView setContentOffset:contentOffsetAfterRotation];
[[self.view viewWithTag:kTemporaryImageTag] removeFromSuperview];
}
Solution 3
The "just snap" answer above didn't work for me as it frequently didn't end on the item that was in view before the rotate. So I derived a flow layout that uses a focus item (if set) for calculating the content offset. I set the item in willAnimateRotationToInterfaceOrientation and clear it in didRotateFromInterfaceOrientation. The inset adjustment seems to be need on IOS7 because the Collection view can layout under the top bar.
@interface HintedFlowLayout : UICollectionViewFlowLayout
@property (strong)NSIndexPath* pathForFocusItem;
@end
@implementation HintedFlowLayout
-(CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset
{
if (self.pathForFocusItem) {
UICollectionViewLayoutAttributes* layoutAttrs = [self layoutAttributesForItemAtIndexPath:self.pathForFocusItem];
return CGPointMake(layoutAttrs.frame.origin.x - self.collectionView.contentInset.left, layoutAttrs.frame.origin.y-self.collectionView.contentInset.top);
}else{
return [super targetContentOffsetForProposedContentOffset:proposedContentOffset];
}
}
@end
Solution 4
Swift 4.2 subclass:
class RotatableCollectionViewFlowLayout: UICollectionViewFlowLayout {
private var focusedIndexPath: IndexPath?
override func prepare(forAnimatedBoundsChange oldBounds: CGRect) {
super.prepare(forAnimatedBoundsChange: oldBounds)
focusedIndexPath = collectionView?.indexPathsForVisibleItems.first
}
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
guard let indexPath = focusedIndexPath
, let attributes = layoutAttributesForItem(at: indexPath)
, let collectionView = collectionView else {
return super.targetContentOffset(forProposedContentOffset: proposedContentOffset)
}
return CGPoint(x: attributes.frame.origin.x - collectionView.contentInset.left,
y: attributes.frame.origin.y - collectionView.contentInset.top)
}
override func finalizeAnimatedBoundsChange() {
super.finalizeAnimatedBoundsChange()
focusedIndexPath = nil
}
}
Solution 5
For those using iOS 8+, willRotateToInterfaceOrientation and didRotateFromInterfaceOrientation are deprecated.
You should use the following now:
/*
This method is called when the view controller's view's size is changed by its parent (i.e. for the root view controller when its window rotates or is resized).
If you override this method, you should either call super to propagate the change to children or manually forward the change to children.
*/
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator
{
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
// Update scroll position during rotation animation
self.collectionView.contentOffset = (CGPoint){contentOffsetX, contentOffsetY};
} completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
// Whatever you want to do when the rotation animation is done
}];
}
Swift 3:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { (context:UIViewControllerTransitionCoordinatorContext) in
// Update scroll position during rotation animation
}) { (context:UIViewControllerTransitionCoordinatorContext) in
// Whatever you want to do when the rotation animation is done
}
}
Tobias Kräntzer
Updated on July 05, 2022Comments
-
Tobias Kräntzer almost 2 years
I'm trying to handle interface orientation changes in a UICollectionViewController. What I'm trying to achieve is, that I want to have the same contentOffset after an interface rotation. Meaning, that it should be changed corresponding to the ratio of the bounds change.
Starting in portrait with a content offset of {bounds.size.width * 2, 0} …
… should result to the content offset in landscape also with {bounds.size.width * 2, 0} (and vice versa).
Calculating the new offset is not the problem, but don't know, where (or when) to set it, to get a smooth animation. What I'm doing so fare is invalidating the layout in
willRotateToInterfaceOrientation:duration:
and resetting the content offset indidRotateFromInterfaceOrientation:
:- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration; { self.scrollPositionBeforeRotation = CGPointMake(self.collectionView.contentOffset.x / self.collectionView.contentSize.width, self.collectionView.contentOffset.y / self.collectionView.contentSize.height); [self.collectionView.collectionViewLayout invalidateLayout]; } - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation; { CGPoint newContentOffset = CGPointMake(self.scrollPositionBeforeRotation.x * self.collectionView.contentSize.width, self.scrollPositionBeforeRotation.y * self.collectionView.contentSize.height); [self.collectionView newContentOffset animated:YES]; }
This changes the content offset after the rotation.
How can I set it during the rotation? I tried to set the new content offset in
willAnimateRotationToInterfaceOrientation:duration:
but this results into a very strange behavior.An example can be found in my Project on GitHub.
-
Daniel Galasko over 9 yearsdon't forget that the rotation API was deprecated as of iOS 8]
-
Valeriy Van over 9 yearsYour answer is brilliant! But you missed calls to super in willRotateToInterfaceOrientation and didRotateFromInterfaceOrientation.
-
mwright over 8 yearsThis didn't work for me as it was, I instead changed it to not calculate an offset after rotation, instead I store the NSIndexPath pulled when getting cell information and instead of setting the offset I scroll to that with no animation. If someone has interest I can put together an actual answer with code (swift).
-
mwright over 8 yearsI ended up using something like this as well, wish I'd seen yours first
-
Alex Cio over 8 yearsPlease describe what you have done or tell anything about your project!
-
Just a coder about 8 yearsby far the simplest solution
-
Just a coder about 7 yearsis the call to super needed though?
-
Kenan Begić almost 7 yearsExact and precise answer. Thanks a lot!
-
iDoc over 6 yearsAfter quite a bit of research this is the most elegant solution!
-
iTux over 6 yearsHow about fullscreen cells ? When I slide to second or third cell, and rotate I see previous cell on last, and last on second :(
-
Nathan Barreto almost 6 yearsIt works. You just have to make sure that viewWillTransition is being called. Other point is if the next page is not a "entire" page you just need to take of the round on index variable.
-
Patrick Roberts almost 6 yearsI implemented this in Xamarin, works great in iOS 11
-
Mario about 5 yearsShould you be using the
floor
function instead ofround
? -
Gurjinder Singh about 5 years@Mario I am using round only
-
Daniel Williams over 4 yearsThis is more what I was looking for. I wanted rotation code to be contained within my custom UICollectionView subclass, so that rotation code wouldn't need to be duplicated throughout the app. This does cause scrolling to occur though, which means cell re-use kicks in. That may or may not have unintentional side-effects.
-
dronpopdev about 4 yearsInstead of calling
collectionView.reloadData()
you can callcollectionView.collectionViewLayout.invalidateLayout()
-
aheze over 2 yearsUnfortunately,
targetContentOffset
isn't called for me