Reputation: 6971
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
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
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
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
.
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
Reputation: 832
This may be related.
[NSIndexPath indexPathForRow: NSNotFound inSection: index]
Use NSNotFound instead of 0.
Upvotes: 1
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
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
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