emenegro
emenegro

Reputation: 6971

Scroll to item in collection view crashes the app

I want to scroll to a certain item of an UICollectionView inside viewWillAppear

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

    [collectionView_ scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:selectedIndex_ inSection:0]
                            atScrollPosition:UICollectionViewScrollPositionLeft
                                    animated:NO];
}

On iOS 6 this code crashes the app returning

*** Assertion failure in -[UICollectionViewData layoutAttributesForItemAtIndexPath:], /SourceCache/UIKit_Sim/UIKit-2372/UICollectionViewData.m:485
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'must return a UICollectionViewLayoutAttributes instance from -layoutAttributesForItemAtIndexPath: for path <NSIndexPath 0x13894e70> 2 indexes [0, 2]'

On iOS7 it does not crashes but is simply does nothing.

The scrolling to the correct item only works in viewDidAppear but I want to show the screen with the collection in the correct item, on appear. I tried to scroll it in viewDidLayoutSubviews but it also crashes. Wrapping the call inside a try-catch avoids the crash but it still does not working.

What is the point of this? Is it impossible to show the correct item on appear?

Thank you so much.

EDIT 1

I printed this on viewWillAppear and viewDidLayoutSubviews (selectedIndex_ is 2, and the collection has 10 items):

UICollectionViewLayoutAttributes *test = [collectionView_ layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForRow:selectedIndex_ inSection:0]];

The result is this in both places.

<UICollectionViewLayoutAttributes: 0x11b9ff20> index path: (<NSIndexPath: 0x11b9c450> {length = 2, path = 0 - 2}); frame = (0 0; 0 0);

EDIT 2

This is the trace I print of the contentSize of the collection

2013-12-09 08:56:59.300 - didLoad {0, 0}
2013-12-09 08:56:59.315 - willAppear {0, 0}
2013-12-09 08:56:59.350 - viewDidLayoutSubviews {0, 0}
2013-12-09 08:56:59.781 - viewDidLayoutSubviews {3200, 223}
2013-12-09 08:56:59.879 - didAppear {3200, 223}
2013-12-09 08:56:59.882 - viewDidLayoutSubviews {3200, 223}

The collection view is created programatically in viewDidLoad

UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
[layout setScrollDirection:UICollectionViewScrollDirectionHorizontal];
collectionView_ = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
[collectionView_ setTranslatesAutoresizingMaskIntoConstraints:NO];
[collectionView_ setDelegate:self];
[collectionView_ setDataSource:self];
[collectionView_ setShowsHorizontalScrollIndicator:NO];
[collectionView_ setPagingEnabled:YES];
[collectionView_ setBackgroundColor:[UIColor whiteColor]];
[collectionView_ registerClass:[MyCollectionViewCell class] forCellWithReuseIdentifier:[MyCollectionViewCell collectionCellIdentifier]];
[scrollView_ addSubview:collectionView_];

scrollView_ is created via XIB (the only control in the XIB. I need another scroll to put some other control below the horizontal collection). The constraints of this method are set in updateViewConstraints

- (void)updateViewConstraints {
    [super updateViewConstraints];

    NSDictionary *views = [self viewsDictionary];
    NSDictionary *metrics = @{ @"bigMargin" : @12, @"collectionViewHeight" : @(collectionViewHeight_) };

    NSMutableString *verticalConstraints = [NSMutableString stringWithString:@"V:|[collectionView_(==collectionViewHeight)]"];

    [scrollView_ addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[collectionView_(==scrollView_)]|"
                                                                        options:0
                                                                        metrics:nil
                                                                          views:views]];

    if (extendedInformationView_) {

        [scrollView_ addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[extendedInformationView_(==scrollView_)]|"
                                                                            options:0
                                                                            metrics:nil
                                                                              views:views]];

        [verticalConstraints appendFormat:@"-bigMargin-[extendedInformationView_]"];
    }

    if (actionListView_) {

        [scrollView_ addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[actionListView_(==scrollView_)]|"
                                                                            options:0
                                                                            metrics:nil
                                                                              views:views]];

        [verticalConstraints appendFormat:@"-bigMargin-[actionListView_]"];
    }

    [verticalConstraints appendString:@"-bigMargin-|"];

    [scrollView_ addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:verticalConstraints
                                                                        options:0
                                                                        metrics:metrics
                                                                          views:views]];

}

MyCollectionViewCell creates all its controls in its initWithFrame method, and here is the method to return the cell.

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {

    MyCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:[MyCollectionViewCell collectionCellIdentifier]
                                                                           forIndexPath:indexPath];

    // Data filling

    return cell;   
}

Upvotes: 18

Views: 10829

Answers (7)

iPili85
iPili85

Reputation: 86

I had the same problem and could solve it. First of all, when you create the UICollectionView you must specify a frame with its width, no matters the height, but the width it's very important to scroll to the correct item.

UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
[layout setScrollDirection:UICollectionViewScrollDirectionHorizontal];
collectionView_ = [[UICollectionView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, CGRectGetWidth([scrollView_ frame]), 0.0f)
                                     collectionViewLayout:layout];
[collectionView_ setDelegate:self];
[collectionView_ setDataSource:self];
[collectionView_ setBackgroundColor:[UIColor clearColor]];
[collectionView_ setTranslatesAutoresizingMaskIntoConstraints:NO];
[collectionView_ setShowsHorizontalScrollIndicator:NO];
[collectionView_ setPagingEnabled:YES];
[scrollView_ addSubview:collectionView_];

After creating the UICollectionView you must tell the view that needs update its constraints, because in iOS6 you have to force it, so invoke updateViewConstraints:

[self updateViewConstraints]

Override the method updateViewConstraints, and set here all the view constraints. Remember to remove all the constraints of the view before invoke super (in your code you are not removing them), and set on metrics' dictionary the width of the UICollectionView and don't use [collectionView_(==scrollView_)] because sometimes it fails, mainly in iOS6.

- (void)updateViewConstraints {

    [scrollView_ removeConstraints:[scrollView_ constraints]];
    [super updateViewConstraints];

    NSDictionary *views = [self viewsDictionary];
    NSDictionary *metrics = @{ @"bigMargin" : @12, @"collectionViewHeight" : @(collectionViewHeight_), @"viewWidth" : @(CGRectGetWidth([scrollView_ frame]) };

    NSMutableString *verticalConstraints = [NSMutableString stringWithString:@"V:|[collectionView_(==collectionViewHeight)]"];

    [scrollView_ addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[collectionView_(==viewWidth)]|"
                                                                        options:0
                                                                        metrics:nil
                                                                          views:views]];

    if (extendedInformationView_) {

        [scrollView_ addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[extendedInformationView_(==scrollView_)]|"
                                                                            options:0
                                                                            metrics:nil
                                                                              views:views]];

        [verticalConstraints appendFormat:@"-bigMargin-[extendedInformationView_]"];
    }

    if (actionListView_) {

        [scrollView_ addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[actionListView_(==scrollView_)]|"
                                                                            options:0
                                                                            metrics:nil
                                                                              views:views]];

        [verticalConstraints appendFormat:@"-bigMargin-[actionListView_]"];
    }

    [verticalConstraints appendString:@"-bigMargin-|"];

    [scrollView_ addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:verticalConstraints
                                                                        options:0
                                                                        metrics:metrics
                                                                          views:views]];

}

Finally, to scroll the UICollectionView to the correct item, do it on viewWillLayoutSubviews and don't forget to check if UICollectionView's size is not zero to avoid app crash:

- (void)viewWillLayoutSubviews {
    [super viewWillLayoutSubviews];

    if (!CGSizeEqualToSize([collectionView_ frame].size, CGSizeZero)) {

        [collectionView_ scrollToItemAtIndexPath:_selectedRowItem_ inSection:0]
                                atScrollPosition:UICollectionViewScrollPositionLeft
                                        animated:NO];
    }
}

That's all. Hope it helps!

Upvotes: 7

Sathish Kumar
Sathish Kumar

Reputation: 229

Make sure that collection view content is loaded in viewWillAppear. CollectionView/TableView will load the data when view is appeared on the screen. Try scroll to item in viewDidAppear: method or use some delay.

Upvotes: 1

art-divin
art-divin

Reputation: 1645

I've been able to reproduce your problem. The problem is that UICollectionView doesn't know about its content size at the moment you are trying to scroll to specified NSIndexPath.

Here's a code to reproduce problem:

@interface TLCollectionViewController : UIViewController

@end

@interface CollectionCell : UICollectionViewCell

@property (nonatomic, strong) UILabel *titleLbl;

@end

@implementation CollectionCell

- (id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        _titleLbl = [[UILabel alloc] init];

        [_titleLbl setTranslatesAutoresizingMaskIntoConstraints:NO];
        [self.contentView addSubview:_titleLbl];

        NSArray *titleLblHConstrArr = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[titleLbl]|" options:kNilOptions metrics:nil views:@{ @"titleLbl" : _titleLbl }];
        NSArray *titleLblVConstrArr = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[titleLbl]|" options:kNilOptions metrics:nil views:@{ @"titleLbl" : _titleLbl }];

        [[self contentView] addConstraints:titleLblHConstrArr];
        [[self contentView] addConstraints:titleLblVConstrArr];

        [self setBackgroundColor:[UIColor whiteColor]];
    }
    return self;
}

- (void)prepareForReuse {
    [super prepareForReuse];
    self.titleLbl.text = @"";
}

@end

@interface TLCollectionViewController () <UICollectionViewDataSource>

@property (nonatomic, strong) NSArray *items;
@property (nonatomic, strong) UICollectionView *collView;

@end

@implementation TLCollectionViewController

- (void)loadView {
    self.view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height)];
    [self.view setBackgroundColor:[UIColor whiteColor]];
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.items = @[ @"one", @"two", @"three", @"one", @"two", @"three", @"one", @"two", @"three"
                    , @"one", @"two", @"three", @"one", @"two", @"three", @"one", @"two", @"three", @"one", @"two", @"three"
                    , @"one", @"two", @"three", @"one", @"two", @"three", @"one", @"two", @"three", @"one", @"two", @"three"
                    , @"one", @"two", @"three", @"one", @"two", @"three", @"one", @"two", @"three", @"one", @"two", @"three"
                    , @"one", @"two", @"three", @"one", @"two", @"three", @"one", @"two", @"three", @"one", @"two", @"three"
                    , @"one", @"two", @"three", @"one", @"two", @"three", @"one", @"two", @"three", @"one", @"two", @"three"
                    , @"one", @"two", @"three", @"one", @"two", @"three", @"one", @"two", @"three", @"one", @"two", @"three"
                    , @"one", @"two", @"three", @"one", @"two", @"three", @"one", @"two", @"three", @"one", @"two", @"three" ];

    UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
    [layout setScrollDirection:UICollectionViewScrollDirectionHorizontal];
    self.collView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
    [self.collView setDataSource:self];
    [self.collView setTranslatesAutoresizingMaskIntoConstraints:NO];
    [self.collView registerClass:[CollectionCell class] forCellWithReuseIdentifier:@"collCell"];

    [self.view addSubview:self.collView];

    NSArray *collViewHConstrArr = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[collView(==300)]" options:kNilOptions metrics:nil views:@{ @"collView" : self.collView }];
    NSArray *collViewVConstrArr = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[collView(==300)]" options:kNilOptions metrics:nil views:@{ @"collView" : self.collView }];

    [self.view addConstraints:collViewHConstrArr];
    [self.view addConstraints:collViewVConstrArr];
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    // BUG: here on iOS 6 exception is raised, because UICollectionView doesn't know about it's content size and about it's frame
    // but on iOS 7 it does know about it's frame, thus makes it possible to know about attributes
    id attr = [self.collView layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForRow:70 inSection:0]];
    [self.collView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:70 inSection:0]
                          atScrollPosition:UICollectionViewScrollPositionLeft
                                  animated:NO];
}

#pragma mark - UICollectionViewDataSource

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
    return self.items.count;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    CollectionCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"collCell" forIndexPath:indexPath];
    cell.titleLbl.text = self.items[indexPath.row];
    return cell;
}

@end

This behaviour is different on iOS 6 and iOS 7. In iOS 6 if you try to get attributes with UICollectionView that doesn't have content size or frame, you'll get an NSInternalInconsistencyException exception. As for iOS 7 this was somehow changed, and now you have to know neither about UICollectionView content size, nor about it's frame in order to get attributes for specific NSIndexPath.

Regarding call of -[UICollectionViewData layoutAttributesForItemAtIndexPath:] - this method gets called automatically while trying to perform any kind of scrolling of UICollectionView.

enter image description here

To answer your question:

Is it impossible to show the correct item on appear?

Yes, it is impossible to do it there. You have to know layout in order to be able to scroll correctly. Correct way to perform scroll is to implement it in -[UIViewController viewDidLayoutSubviews]:

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];
    [self.collView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:70 inSection:0]
                      atScrollPosition:UICollectionViewScrollPositionLeft
                              animated:NO];
}

Upvotes: 2

0xFADE
0xFADE

Reputation: 832

This may be related.

[NSIndexPath indexPathForRow: NSNotFound inSection: index]

Use NSNotFound instead of 0.

Upvotes: 1

Martin
Martin

Reputation: 1135

This is where a random extra retain would work in the olden days. I've had it a few times where memory is deallocated and reallocated so the app is looking for something at an address that has been recycled leading to an internal inconsistency error.

There is probably a far better way to add a strong (a __strong probably) it but I try to keep properties like this as, well, properties and:

@property (nonatomic,strong) UICollectionView *collectionView_;

Will keep a strong reference to the data hopefully stopping the inconsistency thing.

Martin

Upvotes: 1

keywind
keywind

Reputation: 1195

It seems that UIKit would crash when -scrollToItemAtIndexPath:atScrollPosition:Animated is called when the UICollectionView is not yet laid out, as you could see on Radar

So you can only put it in viewDidAppear and and viewDidLayoutSubviews on iOS7 and only put it in viewDidAppear on iOS6. why even viewDidLayoutSubviews is excluded on iOS6 is shown in your log:

2013-12-09 08:56:59.300 - didLoad {0, 0}
2013-12-09 08:56:59.315 - willAppear {0, 0}
2013-12-09 08:56:59.350 - viewDidLayoutSubviews {0, 0}
2013-12-09 08:56:59.781 - viewDidLayoutSubviews {3200, 223}
2013-12-09 08:56:59.879 - didAppear {3200, 223}
2013-12-09 08:56:59.882 - viewDidLayoutSubviews {3200, 223}

When the first time viewDidLayoutSubviews is called, UICollectionView is not yet laid out. iOS7 just work in the second time viewDidLayoutSubviews is called, but iOS 6 will crash at the first time.

Upvotes: 1

Oliver Atkinson
Oliver Atkinson

Reputation: 8029

The frame of your collection view initially is CGRectZero, for the flow-layout to work it needs to have a collection view with a frame. This seems to happen when you update the layout constraints which is too late in the view life-cycle.

collectionView_ = [[UICollectionView alloc] initWithFrame:self.view.bounds collectionViewLayout:layout];

Hope this helps.

Upvotes: 3

Related Questions