According to Apple\'s documentation (and touted at WWDC 2012), it is possible to set the layout on UICollectionView
dynamically and even animate the changes:
If it helps add to the body of experience: I encountered this problem persistently regardless of the size of my content, whether I had set a content inset, or any other obvious factor. So my workaround was somewhat drastic. First I subclassed UICollectionView and added to combat inappropriate content offset setting:
- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated
{
if(_declineContentOffset) return;
[super setContentOffset:contentOffset];
}
- (void)setContentOffset:(CGPoint)contentOffset
{
if(_declineContentOffset) return;
[super setContentOffset:contentOffset];
}
- (void)setCollectionViewLayout:(UICollectionViewLayout *)layout animated:(BOOL)animated
{
_declineContentOffset ++;
[super setCollectionViewLayout:layout animated:animated];
_declineContentOffset --;
}
- (void)setContentSize:(CGSize)contentSize
{
_declineContentOffset ++;
[super setContentSize:contentSize];
_declineContentOffset --;
}
I'm not proud of it but the only workable solution seems to be completely to reject any attempt by the collection view to set its own content offset resulting from a call to setCollectionViewLayout:animated:
. Empirically it looks like this change occurs directly in the immediate call, which obviously isn't guaranteed by the interface or the documentation but makes sense from a Core Animation point of view so I'm perhaps only 50% uncomfortable with the assumption.
However there was a second issue: UICollectionView was now adding a little jump to those views that were staying in the same place upon a new collection view layout — pushing them down about 240 points and then animating them back to the original position. I'm unclear why but I modified my code to deal with it nevertheless by severing the CAAnimation
s that had been added to any cells that, actually, weren't moving:
- (void)setCollectionViewLayout:(UICollectionViewLayout *)layout animated:(BOOL)animated
{
// collect up the positions of all existing subviews
NSMutableDictionary *positionsByViews = [NSMutableDictionary dictionary];
for(UIView *view in [self subviews])
{
positionsByViews[[NSValue valueWithNonretainedObject:view]] = [NSValue valueWithCGPoint:[[view layer] position]];
}
// apply the new layout, declining to allow the content offset to change
_declineContentOffset ++;
[super setCollectionViewLayout:layout animated:animated];
_declineContentOffset --;
// run through the subviews again...
for(UIView *view in [self subviews])
{
// if UIKit has inexplicably applied animations to these views to move them back to where
// they were in the first place, remove those animations
CABasicAnimation *positionAnimation = (CABasicAnimation *)[[view layer] animationForKey:@"position"];
NSValue *sourceValue = positionsByViews[[NSValue valueWithNonretainedObject:view]];
if([positionAnimation isKindOfClass:[CABasicAnimation class]] && sourceValue)
{
NSValue *targetValue = [NSValue valueWithCGPoint:[[view layer] position]];
if([targetValue isEqualToValue:sourceValue])
[[view layer] removeAnimationForKey:@"position"];
}
}
}
This appears not to inhibit views that actually do move, or to cause them to move incorrectly (as if they were expecting everything around them to be down about 240 points and to animate to the correct position with them).
So this is my current solution.
UICollectionViewLayout
contains the overridable method targetContentOffsetForProposedContentOffset: which allows you to provide the proper content offset during a change of layout, and this will animate correctly. This is available in iOS 7.0 and above
I have been pulling my hair out over this for days and have found a solution for my situation that may help.
In my case I have a collapsing photo layout like in the photos app on the ipad. It shows albums with the photos on top of each other and when you tap an album it expands the photos. So what I have is two separate UICollectionViewLayouts and am toggling between them with [self.collectionView setCollectionViewLayout:myLayout animated:YES]
I was having your exact problem with the cells jumping before animation and realized it was the contentOffset
. I tried everything with the contentOffset
but it still jumped during animation. tyler's solution above worked but it was still messing with the animation.
Then I noticed that it happens only when there were a few albums on the screen, not enough to fill the screen. My layout overrides -(CGSize)collectionViewContentSize
as recommended. When there are only a few albums the collection view content size is less than the views content size. That's causing the jump when I toggle between the collection layouts.
So I set a property on my layouts called minHeight and set it to the collection views parent's height. Then I check the height before I return in -(CGSize)collectionViewContentSize
I ensure the height is >= the minimum height.
Not a true solution but it's working fine now. I would try setting the contentSize
of your collection view to be at least the length of it's containing view.
edit: Manicaesar added an easy workaround if you inherit from UICollectionViewFlowLayout:
-(CGSize)collectionViewContentSize { //Workaround
CGSize superSize = [super collectionViewContentSize];
CGRect frame = self.collectionView.frame;
return CGSizeMake(fmaxf(superSize.width, CGRectGetWidth(frame)), fmaxf(superSize.height, CGRectGetHeight(frame)));
}
If you are simply looking for the content offset to not change when transition from layouts, you can creating a custom layout and override a couple methods to keep track of the old contentOffset and reuse it:
@interface CustomLayout ()
@property (nonatomic) NSValue *previousContentOffset;
@end
@implementation CustomLayout
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset
{
CGPoint previousContentOffset = [self.previousContentOffset CGPointValue];
CGPoint superContentOffset = [super targetContentOffsetForProposedContentOffset:proposedContentOffset];
return self.previousContentOffset != nil ? previousContentOffset : superContentOffset ;
}
- (void)prepareForTransitionFromLayout:(UICollectionViewLayout *)oldLayout
{
self.previousContentOffset = [NSValue valueWithCGPoint:self.collectionView.contentOffset];
return [super prepareForTransitionFromLayout:oldLayout];
}
- (void)finalizeLayoutTransition
{
self.previousContentOffset = nil;
return [super finalizeLayoutTransition];
}
@end
All this is doing is saving the previous content offset before the layout transition in prepareForTransitionFromLayout
, overwriting the new content offset in targetContentOffsetForProposedContentOffset
, and clearing it in finalizeLayoutTransition
. Pretty straightforward