Dynamically setting layout on UICollectionView causes inexplicable contentOffset change

uicollectionview orientation change
uicollectionview recalculate layout
collectionview compositional layout
uicollectionview vertical layout
uicollectionview mosaic layout
uicollectionview reverse layout
uicollectionview 3 columns
uicollectionview card layout

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:

You normally specify a layout object when creating a collection view but you can also change the layout of a collection view dynamically. The layout object is stored in the collectionViewLayout property. Setting this property directly updates the layout immediately, without animating the changes. If you want to animate the changes, you must call the setCollectionViewLayout:animated: method instead.

However, in practice, I've found that UICollectionView makes inexplicable and even invalid changes to the contentOffset, causing cells to move incorrectly, making the feature virtually unusable. To illustrate the problem, I put together the following sample code that can be attached to a default collection view controller dropped into a storyboard:

#import <UIKit/UIKit.h>

@interface MyCollectionViewController : UICollectionViewController
@end

@implementation MyCollectionViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"CELL"];
    self.collectionView.collectionViewLayout = [[UICollectionViewFlowLayout alloc] init];
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return 1;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewCell *cell = [self.collectionView dequeueReusableCellWithReuseIdentifier:@"CELL" forIndexPath:indexPath];
    cell.backgroundColor = [UIColor whiteColor];
    return cell;
}

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
    NSLog(@"contentOffset=(%f, %f)", self.collectionView.contentOffset.x, self.collectionView.contentOffset.y);
    [self.collectionView setCollectionViewLayout:[[UICollectionViewFlowLayout alloc] init] animated:YES];
    NSLog(@"contentOffset=(%f, %f)", self.collectionView.contentOffset.x, self.collectionView.contentOffset.y);
}

@end

The controller sets a default UICollectionViewFlowLayout in viewDidLoad and displays a single cell on-screen. When the cells is selected, the controller creates another default UICollectionViewFlowLayout and sets it on the collection view with the animated:YES flag. The expected behavior is that the cell does not move. The actual behavior, however, is that the cell scroll off-screen, at which point it is not even possible to scroll the cell back on-screen.

Looking at the console log reveals that the contentOffset has inexplicably changed (in my project, from (0, 0) to (0, 205)). I posted a solution for the solution for the non-animated case (i.e. animated:NO), but since I need animation, I'm very interested to know if anyone has a solution or workaround for the animated case.

As a side-note, I've tested custom layouts and get the same behavior.

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)));
}

Remove extra word from README.md · Issue #22 · SwiftKickMobile , However, in practice, I've found that UICollectionView makes inexplicable and even invalid changes to the contentOffset , causing cells to move incorrectly,  UICollectionView is a powerful class allowing your app to manage and customize the layout of views. iOS 10 brings… developer.apple.com Unfortunately, dynamically changing estimatedItemSize does not solve our problem, it only makes it less obvious.

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

targetContentOffset(forProposedContentOffset:) in my - html, 1 changed file 2 additions and 2 deletions The destination `contentOffset` is selected such that the initial visible cells [3]:http://stackoverflow.com/questions/​13780138/dynamically-setting-layout-on-uicollectionview-causes-inexplicable-  UICollectionView for Springboard like folders (2) I am trying to achieve the following effect: A UICollectionView displays a grid of cells for a parent type of object, e. g. a photo album. When I tap one of these items, I would like to scroll that element to the top of the screen and open a Springboard like folder from it.

This issue bit me as well and it seems to be a bug in the transition code. From what I can tell it tries to focus on the cell that was closest to the center of the pre-transition view layout. However, if there doesn't happen to be a cell at the center of the view pre-transition then it still tries to center where the cell would be post-transition. This is very clear if you set alwaysBounceVertical/Horizontal to YES, load the view with a single cell and then perform a layout transition.

I was able to get around this by explicitly telling the collection to focus on a specific cell (the first cell visible cell, in this example) after triggering the layout update.

[self.collectionView setCollectionViewLayout:[self generateNextLayout] animated:YES];

// scroll to the first visible cell
if ( 0 < self.collectionView.indexPathsForVisibleItems.count ) {
    NSIndexPath *firstVisibleIdx = [[self.collectionView indexPathsForVisibleItems] objectAtIndex:0];
    [self.collectionView scrollToItemAtIndexPath:firstVisibleIdx atScrollPosition:UICollectionViewScrollPositionCenteredVertically animated:YES];
}

iphone, (wrong contentOffset) Dynamically setting layout on UICollectionView causes inexplicable contentOffset change class TimelineCollectionViewFlowLayout:  UICollectionView, introduced in iOS 6, has become one of the most popular UI elements among iOS developers.What makes it so attractive is the separation between the data and presentation layers, which depends on a separate object to handle the layout.

Jumping in with a late answer to my own question.

The TLLayoutTransitioning library provides a great solution to this problem by re-tasking iOS7s interactive transitioning APIs to do non-interactive, layout to layout transitions. It effectively provides an alternative to setCollectionViewLayout, solving the content offset issue and adding several features:

  1. Animation duration
  2. 30+ easing curves (courtesy of Warren Moore's AHEasing library)
  3. Multiple content offset modes

Custom easing curves can be defined as AHEasingFunction functions. The final content offset can be specified in terms of one or more index paths with Minimal, Center, Top, Left, Bottom or Right placement options.

To see what I mean, try running the Resize demo in the Examples workspace and playing around with the options.

The usage is like this. First, configure your view controller to return an instance of TLTransitionLayout:

- (UICollectionViewTransitionLayout *)collectionView:(UICollectionView *)collectionView transitionLayoutForOldLayout:(UICollectionViewLayout *)fromLayout newLayout:(UICollectionViewLayout *)toLayout
{
    return [[TLTransitionLayout alloc] initWithCurrentLayout:fromLayout nextLayout:toLayout];
}

Then, instead of calling setCollectionViewLayout, call transitionToCollectionViewLayout:toLayout defined in the UICollectionView-TLLayoutTransitioning category:

UICollectionViewLayout *toLayout = ...; // the layout to transition to
CGFloat duration = 2.0;
AHEasingFunction easing = QuarticEaseInOut;
TLTransitionLayout *layout = (TLTransitionLayout *)[collectionView transitionToCollectionViewLayout:toLayout duration:duration easing:easing completion:nil];

This call initiates an interactive transition and, internally, a CADisplayLink callback that drives the transition progress with the specified duration and easing function.

The next step is to specify a final content offset. You can specify any arbitrary value, but the toContentOffsetForLayout method defined in UICollectionView-TLLayoutTransitioning provides an elegant way to calculate content offsets relative to one or more index paths. For example, in order to have a specific cell to end up as close to the center of the collection view as possible, make the following call immediately after transitionToCollectionViewLayout:

NSIndexPath *indexPath = ...; // the index path of the cell to center
TLTransitionLayoutIndexPathPlacement placement = TLTransitionLayoutIndexPathPlacementCenter;
CGPoint toOffset = [collectionView toContentOffsetForLayout:layout indexPaths:@[indexPath] placement:placement];
layout.toContentOffset = toOffset;

[self.collectionView setCollectionViewLayout:myLayout animated , Dynamically setting layout on UICollectionView causes inexplicable contentOffset change Layout, Animation, App, This · LayoutAnimationAppThis collectionView setCollectionViewLayout:myLayout animated:YES]. Samuel HinshelwoodApp  UICollectionView layout change when frame size changes. ios,swift,uicollectionview,uicollectionviewlayout. Perhaps if you reset the top of your sectionInset to accommodate for the additional height, i.e. an updated topDist calculation.

Easy.

Animate your new layout and collectionView's contentOffset in the same animation block.

[UIView animateWithDuration:0.3 animations:^{
                                 [self.collectionView setCollectionViewLayout:self.someLayout animated:YES completion:nil];
                                 [self.collectionView setContentOffset:CGPointMake(0, -64)];
                             } completion:nil];

It will keep self.collectionView.contentOffset constant.

ios - Dynamically Set ContentSize of UICollectionView, Dynamically setting layout on UICollectionView causes inexplicable contentOffset change Layout, Animation, App, This. Saved from stackoverflow.​com  If you are using Interface Builder to set a constraint to a view controller’s primary view, Xcode defaults to showing options to set the vertical constraint against the top layout guide. However, if you press ‘Option’, you will see an alternate set of constraints. The constraint for ‘Top Space to Container’ is what you’re looking for.

Confetti Example, ios - Keeping the contentOffset in a UICollectionView while rotating I.. but generally speaking you shouldn't need to change the content size for normal uses. if UIKit has inexplicably applied animations to these views to move them back to where iphone - Dynamically setting layout on UICollectionView causes inexpli. UICollectionView Custom Layout Tutorial: A Spinning Wheel. In this UICollectionView custom layout tutorial, you’ll learn how create a spinning navigation layout, including managing view rotation and scrolling.

Dynamically setting layout on UICollectionView causes inexplicable contentOffset change Layout, Animation, App, This · LayoutAnimationAppThis Or That  Hi there, I'm trying to allow a UICollectionView which has a horizontal flow layout, containing images with content mode of AspectFill, to be dynamically resized in height based on the Y position of another control (a vertical ScrollView) My implmentation works, in that I change the item height based on an event and call invalidate layout In my sublassed UICollectionView I set a custom scroll

Expressions that reference the contentOffset of a containing UIScrollView now update automatically when it changes; Layout now automatically calls setNeedsLayout() on the containing view of the root LayoutNode when its frame changes; You can now reference a macro defined in the root node of a Layout XML file from an expression on the root node

Comments
  • FINALLY SOLVED. @Isaacliu has given the correct answer. I've put in the modern Swift version.
  • I played with this some and came to the conclusion that your workaround is reliable only when the contentSize exactly matches the collectionView's size. So I think it works for non-scrolling collection views.
  • It's almost correct, the only thing you seem to need to ensure that the collectionViewContentSize method returns a size that is at least as big as the collection view. My layout is vertically scrolling so I simply ensured that I return a size that is the right width, followed by the max of the calculated size by my layout and the height of the collection view.
  • In case you inherit from UICollectionViewFlowLayout it is as simple as: - (CGSize)collectionViewContentSize { //Workaround CGSize superSize = [super collectionViewContentSize]; CGRect frame = self.collectionView.frame; return CGSizeMake(fmaxf(superSize.width, CGRectGetWidth(frame)), fmaxf(superSize.height, CGRectGetHeight(frame))); }
  • manicaesar, that one does the trick. It should be added to some answer so it does not get lost in the comments. I added it in the answer.
  • Have a look to targetContentOffsetForProposedContentOffset: and targetContentOffsetForProposedContentOffset:withScrollingVelocity: in your layout subclass.
  • Wish I could upvote this a few more times. For a certain subset of cases involving this issue (e.g. for me, where I want to maintain the current contentOffset), this is the perfect solution. Thanks @cdemiris99.
  • This fixes my immediate issue, but I'm wondering why? Feels like I shouldn't have to do this. Going to get a better understanding of what the deal is here and will write a bug.
  • You are a genius this fixes the issue perfectly!
  • override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint) -> CGPoint { if let collectionView = self.collectionView { let currentContentOffset = collectionView.contentOffset if currentContentOffset.y < proposedContentOffset.y { return currentContentOffset } } return proposedContentOffset } Fixed the problem. Thanks for a hint.
  • I tried this workaround in my sample code and it was very close. The cell only bounced slightly. However, it didn't work as well in other scenarios. For example, change numberOfItemsInSection: to return 100. If you tap the first cell, the view scrolls down by several rows. Then if you tap the first visible cell, the view scrolls back to the top. If you then tap the first cell again, it remains stationary (correct behavior). If you tap a cell on the bottom row, it bounces. Scroll the view down by several rows, tap any row and the view scrolls back to the top.
  • I found using animated:NO in the scrollToItemAtIndexPath: call made things look better.
  • Looks like they've added APIs for controlling this behavior in iOS7.