Karthik Pai
Karthik Pai

Reputation: 597

xcode CollectionViewController scrollToItemAtIndexPath not working

I have created a CollectionView Control and filled it with images. Now I want to scroll to item at a particular index on start. I have tried out scrollToItemAtIndexPath as follows:

[self.myFullScreenCollectionView scrollToItemAtIndexPath:indexPath 
atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];

However, I am getting following exception. Could anyone guide me on where am I going wrong.

2013-02-20 02:32:45.219 ControlViewCollection1[1727:c07] *** Assertion failure in 
-[UICollectionViewData layoutAttributesForItemAtIndexPath:], /SourceCache/UIKit_Sim/UIKit-2380.17
/UICollectionViewData.m:485 2013-02-20 02:32:45.221 ControlViewCollection1[1727:c07] must return a 
UICollectionViewLayoutAttributes instance from -layoutAttributesForItemAtIndexPath: for path 
<NSIndexPath 0x800abe0> 2 indexes [0, 4]

Upvotes: 32

Views: 33630

Answers (9)

followben
followben

Reputation: 9197

Whether it's a bug or a feature, UIKit throws this error whenever scrollToItemAtIndexPath:atScrollPosition:Animated is called before UICollectionView has laid out its subviews.

As a workaround, move your scrolling invocation to a place in the view controller lifecycle where you're sure it has already computed its layout, like so:

@implementation CollectionViewControllerSubclass

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

    // scrolling here doesn't work (results in your assertion failure)
}

- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];

    NSIndexPath *indexPath = // compute some index path

    // scrolling here does work
    [self.collectionView scrollToItemAtIndexPath:indexPath
                                atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally
                                        animated:YES];
}

@end

At the very least, the error message should probably be more helpful. I've opened a rdar://13416281; please dupe.

Upvotes: 79

bcattle
bcattle

Reputation: 12839

This is based on @Womble's answer, all credit goes to them:

The method viewDidLayoutSubviews() gets called repeatedly. For me (iOS 11.2) the first time it gets called the collectionView.contentSize is {0,0}. The second time, the contentSize is correct. Therefore, I had to add a check for this:

var needsDelayedScrolling = false

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    self.needsDelayedScrolling = true
    // ...
}

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    if self.needsDelayedScrolling && collectionView.contentSize.width > 0 {
        self.needsDelayedScrolling = false
        self.collectionView!.scrollToItem(at: someIndexPath,
                at: .centeredVertically,
                animated: false)
        }
    }
}

After adding that extra && collectionView.contentSize.width > 0 it works beautifully.

Upvotes: 0

jwswart
jwswart

Reputation: 1266

Sometimes collectionView(_:didSelectItemAt:) is either not called on the main thread, or blocks it, causing scrollToItem(at:at:animated:) to not do anything.

Work around this by doing:

DispatchQueue.main.async {
  collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
}

Upvotes: 15

Womble
Womble

Reputation: 5289

As of iOS 9.3, Xcode 8.2.1, Swift 3:

Calling scrollToItem(at:) from viewWillAppear() is still broken, particularly if you are using Section Headers/Footers, Auto Layout, Section Insets.

Even if you call setNeedsLayout() and layoutIfNeeded() on the collectionView, the behavior is still borked. Putting the scrolling code in to an animation block doesn't work reliably.

As indicated in the other answers, the solution is to only call scrollToItem(at:) once you are sure everything has been laid out. i.e. in viewDidLayoutSubviews().

However, you need to be selective; you don't want to perform scrolling every time viewWillLayoutSubviews() is called. So a solution is to set a flag in viewWillAppear(), and act it on it in viewDidLayoutSubviews().

i.e.

fileprivate var needsDelayedScrolling = false

override func viewWillAppear(_ animated: Bool)
{
    super.viewWillAppear(animated)
    self.needsDelayedScrolling = true
    // ...
}

override func viewDidLayoutSubviews()
{
    super.viewDidLayoutSubviews()

    if self.needsDelayedScrolling {
        self.needsDelayedScrolling = false
        self.collectionView!.scrollToItem(at: someIndexPath,
                at: .centeredVertically,
                animated: false)
        }
    }
}

Upvotes: 10

Stijn Westerhof
Stijn Westerhof

Reputation: 243

Swift 3

UIView.animate(withDuration: 0.4, animations: {
    myCollectionview.scrollToItem(at: myIndexPath, at: .centeredHorizontally, animated: false)
}, completion: {
    (value: Bool) in
    // put your completion stuff here
})

Upvotes: 0

rghome
rghome

Reputation: 8841

Adding the scrolling logic to viewDidAppear worked for me:

override func viewDidAppear(animated: Bool) {
    super.viewDidAppear(animated)
    self.collectionView?.scrollToItemAtIndexPath(
        someIndexPath,
        atScrollPosition: UICollectionViewScrollPosition.None,
        animated: animated)
}

Adding it to viewDidLoad doesn't work: it gets ignored.

Adding it to viewDidLayoutSubviews doesn't work unless you want to scroll logic calling any time anything changes. In my case, it prevented the user from manually scrolling the item

Upvotes: 3

Code Commander
Code Commander

Reputation: 17310

If you are trying to scroll when the view controller is loading, make sure to call layoutIfNeeded on the UICollectionView before you call scrollToItemAtIndexPath. This is better than putting the scroll logic in viewDidLayoutSubviews because you won't perform the scroll operation every time the parent view's subviews are laid out.

Upvotes: 53

Dmitry Nelepov
Dmitry Nelepov

Reputation: 7306

U can do this and on viewDidLoad method

just make call preformBatchUpdates

[self performBatchUpdates:^{
        if ([self.segmentedDelegate respondsToSelector:@selector(segmentedBar:selectedIndex:)]){
            [self.segmentedDelegate segmentedBar:self selectedIndex:_selectedPage];
        }
    } completion:^(BOOL finished) {
        if (finished){
            [self scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:_selectedPage inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
        }

    }];

In my case my subclass CollectionView has a property selectedPage, and on a setter of this property i call

- (void)setSelectedPage:(NSInteger)selectedPage {
    _selectedPage = selectedPage;
    [self performBatchUpdates:^{
        if ([self.segmentedDelegate respondsToSelector:@selector(segmentedBar:selectedIndex:)]){
            [self.segmentedDelegate segmentedBar:self selectedIndex:_selectedPage];
        }
    } completion:^(BOOL finished) {
        if (finished){
            [self scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:_selectedPage inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
        }

    }];
}

In view controller i calling this by code

- (void)viewDidLoad {
    [super viewDidLoad];
    self.navigationBar.segmentedPages.selectedPage = 1;
}

Upvotes: 7

Alena
Alena

Reputation: 1120

Also remember that you need to use proper UICollectionviewScrollPosition value. Please see code below for clarification:

typedef NS_OPTIONS(NSUInteger, UICollectionViewScrollPosition) {
UICollectionViewScrollPositionNone                 = 0,

/* For a view with vertical scrolling */
// The vertical positions are mutually exclusive to each other, but are bitwise or-able with the horizontal scroll positions.
// Combining positions from the same grouping (horizontal or vertical) will result in an NSInvalidArgumentException.
UICollectionViewScrollPositionTop                  = 1 << 0,
UICollectionViewScrollPositionCenteredVertically   = 1 << 1,
UICollectionViewScrollPositionBottom               = 1 << 2,

/* For a view with horizontal scrolling */
// Likewise, the horizontal positions are mutually exclusive to each other.
UICollectionViewScrollPositionLeft                 = 1 << 3,
UICollectionViewScrollPositionCenteredHorizontally = 1 << 4,
UICollectionViewScrollPositionRight                = 1 << 5

};

Upvotes: 5

Related Questions