Start UICollectionView at a specific indexpath
Asked Answered
P

11

54

I currently have a collection view that does horizontal paging where each cell is fullscreen. What I want to do is for the collectionview to start at a specific index when it shows.

Right now I'm using scrollToItemAtIndexPath:atScrollPosition:animated: with animated set to NO but that still loads the first index first before it can scroll to the specific item. It also seems I can only use this method in ViewDidAppear so it shows the first cell and then blinks to the cell that I want to show. I hide this by hiding the collection view until the scroll has finished but it doesn't seem ideal.

Is there any better way to do this other than the way I described it?

Thanks!

Pentheas answered 6/8, 2013 at 17:51 Comment(5)
Why not do it in -viewWillAppear? Also you can set the animation to NO to get instantaneous scrollingCrumley
I've tried -viewWillAppear that but it gives me this error: *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'must return a UICollectionViewLayoutAttributes instance from -layoutAttributesForItemAtIndexPath: for path <NSIndexPath 0x85813a0> 2 indexes [0, 0]Pentheas
Is there any updates on this issue? I am also facing this issue. Any help?Bengali
maybe of changing how the collection view loads the data, you could re-arrange the data itself to be in the order you want ?Decorate
@Pentheas - Have you find the solution of above question . I am suffering from same problem please help me.Singley
J
67

So I solved this a different way, using the UICollectionViewDelegate method and a one-off Bool:

Swift 2:

var onceOnly = false

internal func collectionView(collectionView: UICollectionView, willDisplayCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {
    if !onceOnly {
        let indexToScrollTo = NSIndexPath(forRow: row, inSection: section)
        self.problemListCollectionView.scrollToItemAtIndexPath(indexToScrollTo, atScrollPosition: .Left, animated: false)
        onceOnly = true
    }

}

Swift 3:

  var onceOnly = false

  internal func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    if !onceOnly {
      let indexToScrollTo = IndexPath(item: row, section: section)
      self.problemListCollectionView.scrollToItem(at: indexToScrollTo, at: .left, animated: false)
      onceOnly = true
    }
  }

This code is executed before any animation occurs (so it really loads to this point), which is better than attempting to call in viewDidAppear, and I didn't have success with it in viewWillAppear.

Jampan answered 28/2, 2016 at 7:21 Comment(9)
Works! But FYI, GetCell is still called for position zero but before its displayed on screen willDisplayCell is called which scrolls, which calls GetCell for the desired position, which is then displayed.Kevyn
@sschale, Thanks! You have saved my time :)Displume
Thank you for solution! It is perfect!Rhyton
I didn't understand.. Shall I all this in viewDidAppear or not?Cthrine
@pavlos this is a delegate method which is automatically invoked.Jampan
@Jampan the problem with Swift 3 example, the item: row is not declared as I can see now when I try this exampleCthrine
It's up to you to define row and sectionJampan
Perfect Solution.. Thank youForeleg
You can also reference the collectionView directly from the delegate method, instead of using self.problemListCollectionView, if it fits your use case that is :)Apoenzyme
S
21

To solve this problem I partially used the greenhouse answer.

/// Edit
var startIndex: Int! = 0

override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)

    collectionView.setNeedsLayout()
    collectionView.layoutIfNeeded()

    collectionView.scrollToItemAtIndexPath(
        NSIndexPath(forItem: 0, inSection: startIndex),
        atScrollPosition: .None,
        animated: false)
 }

The problem seems to be in the wrong collectionView size. After setting the layout scrollToItemAtIndexPath produces the needed result.

It also seems that this problem only persists when a Collection View is used inside a UIViewController.

Silique answered 7/10, 2015 at 10:23 Comment(2)
can you declare startIndex please?Cthrine
@PavlosNicolaou I've added the startIndex variable in the code example. Check it out. You would change the value of startIndex in the prepareForSegue(...) method when segueing to the controller containing the collection.Silique
R
11

Unfortunately, every single one of these existing answers is at least partly wrong or does not answer the exact question being asked. I worked through this issue with a co-worker who was not helped by any of these responses.

All you need to do is set the content offset without animation to the correct content offset and then call reload data. (Or skip the reloadData call if it has not been loaded at all yet.) You should do this in viewDidLoad if you never want the first cell to be created.

This answer assumes the collection view scrolls horizontally and the size of the cells are the same size as the view but the concept is the same if you want to scroll vertically or the cells are a different size. Also if your CollectionView has more than one section you have to do a bit more math to calculate the content offset but the concept is still the same.

func viewDidLoad() {
    super.viewDidLoad()
    let pageSize = self.view.bounds.size
    let contentOffset = CGPoint(x: pageSize.width * self.items.count, y: 0)
    self.collectionView.setContentOffset(contentOffset, animated: false)
}
Ruprecht answered 21/4, 2018 at 1:21 Comment(3)
This one should be the accepted answer. Works like a charm!Windgall
I think offsetting the content isn't the same as going to an index.Vladimar
It is a very good solution, thanks. Exactly what I needed. Only in my specific implementation I used it in other place instead viewDidLoad, anyway it works good.Tutorial
J
7

A simpler solution inspired by others:

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    DispatchQueue.main.async {
        self.collectionView.scrollToItem(at: lastIndexPath, section: 0), at: .centeredHorizontally, animated: false)
    }
}

It will work if you put the code inside DispatchQueue.main.async block.

Jerlenejermain answered 24/10, 2018 at 11:11 Comment(2)
This should not work but it does. wth.Shirtwaist
This did the trick! Thanks!Pneumatophore
R
6

Swift 3.0 tested and works.

var onceOnly = false
    internal func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
        if !onceOnly {
            //set the row and section you need.
            let indexToScrollTo = IndexPath(row: 1, section: indexPath.section)
            self.fotmCollectionView.scrollToItem(at: indexToScrollTo, at: .left, animated: false)
            onceOnly = true
        }
    }
Regolith answered 10/10, 2016 at 14:23 Comment(2)
do you have any idea why this doesn't work for me? I change the .left with .bottom because I want the to scroll at the bottom of the collectionViewCthrine
You actually want to use .top because that means it will bring that specific cell to the top position of the screen, which may be what you want. I'm assuming you have a vertical scrolling collection view. If you want the cell to appear at the bottom of the screen then use .bottom. .left and .right are used for horizontal scrolling collection views.Papyraceous
P
3

Here is what worked for me (in a UICollectionViewController class):

private var didLayoutFlag: Bool = false

public override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    if let collectionView = self.collectionView {
        if !self.didLayoutFlag {
            collectionView.scrollToItemAtIndexPath(self.viewModel.initialIndexPath, atScrollPosition: .None, animated: false)
            self.didLayoutFlag = true
        }
    }
}
Potation answered 21/3, 2016 at 15:52 Comment(0)
N
2

Pass the indexPath from the first VC to the collection view in the DidSelectItemAtIndexPath method. In viewDidLoad of your collection view, use the method scrollToItemAtIndexPath:atScrollPosition:animated: Set animated to NO and atScrollPosition to UICollectionViewScrollPositionNone. Like this:

[self.collectionView scrollToItemAtIndexPath:self.indexPathFromVC atScrollPosition:UICollectionViewScrollPositionNone animated:NO];
Nette answered 22/9, 2014 at 8:29 Comment(2)
it seems like this solution is only viable if you are using a UICollectionViewController. what if you are using just a UICollectionView inside a different view controller? (in this scenario, viewDidLoad gets called before the underlying UICollectionView is rendered (its nil at the point viewDidLoad is invoked)Pomelo
i figured out what worked for me, and posted an answerPomelo
P
2

this seemed to work for me:

- (void)viewDidLayoutSubviews
{     
   [self.collectionView layoutIfNeeded];
   NSArray *visibleItems = [self.collectionView indexPathsForVisibleItems];
   NSIndexPath *currentItem = [visibleItems objectAtIndex:0];
   NSIndexPath *nextItem = [NSIndexPath indexPathForItem:someInt inSection:currentItem.section];

   [self.collectionView scrollToItemAtIndexPath:nextItem atScrollPosition:UICollectionViewScrollPositionNone animated:YES];
}
Pomelo answered 28/5, 2015 at 18:54 Comment(1)
This will be called every time something changes: orientation, scrolling ... Not that what you want? If this is in the main view controller (which hosts the collection view), then on orientation change this still will be called (perhaps multiple times).Snowber
S
0

I came here having this same issue and found that in my case, this issue was caused my the ViewController in my storyboard being set as 'freeform' size.

I guess viewWillLayoutSubviews gets called to calculate the correct size when the view is first loaded if the storyboard's dimensions leave this unclear. (I had sized my viewController in my storyboard as to be 'freeform' so I could make it very tall to see/edit many cells in long tableView inside my collectionView).

I found that victor.vasilica's & greenhouse's approach re: putting the 'scrollToRow' command in viewWillLayoutSubviews did work perfectly to fix the issue.

However, I also found that once I made the VC in my storyboard 'fixed' size again, the issue immediately went away and I was able to set the initial cell from viewWillAppear. Your situation may be different, but this helped me understand what was going on in my situation and I hope my answer might help inform others with this issue.

Sivan answered 22/3, 2017 at 23:46 Comment(0)
K
0

Just found me in the same problem, and make it work by adding this piece of code in willDisplayCell delegate call

private var firstLoad: Bool = true

func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    if self.firstLoad {
        let initialIndexPath = <initial index path>
        collectionView.scrollToItem(
            at: initialIndexPath, 
            at: <UICollectionView.ScrollPosition>, 
            animated: false
        )
        self.firstLoad = false
    }
}
Kailyard answered 30/4, 2020 at 13:37 Comment(0)
R
-1

Hey I have Solved With Objective c . I think this is useful for you. You can convert Objective c to swift as well . Here is my Code:

**In My case , On button click I activate the specific index , that is 3 **

for Vertical

 - (IBAction)Click:(id *)sender {
    NSInteger index=3;
    CGFloat pageHeight = self.collectionView.frame.size.height;
    CGPoint scrollTo = CGPointMake(0, pageHeight * index);
    [self.collectionView setContentOffset:scrollTo animated:YES];
}

For Horizontal

 - (IBAction)Click:(id *)sender {
    NSInteger index=3;
    CGFloat pageWidth = self.collectionView.frame.size.width;
    CGPoint scrollTo = CGPointMake(pageWidth * index, 0);
    [self.collectionView setContentOffset:scrollTo animated:YES];
}

I hope it may help You.

Reach answered 29/11, 2016 at 7:49 Comment(1)
whats about Swift 3?Cthrine

© 2022 - 2024 — McMap. All rights reserved.