Does anyone know why contentSize is not updated immediately after reloadData is called on UICollectionView?
If you need to know the contentSize the best work around I've found is the following:
[_collectionView reloadData];
double delayInSeconds = 0.0001;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(DISPATCH_TIME_NOW, dispatch_get_main_queue(), ^(void)
{
// TODO: Whatever it is you want to do now that you know the contentSize.
});
Obviously this is a fairly brittle hack that makes assumptions on Apple's implementation but so far it has proven to work quite reliably.
Does anyone have any other workarounds or knowledge on why this happens? I'm debating submitting a radar because this I can't understand why they cannot calculate the contentSize in the same run loop. That is how UITableView has worked for its entire implementation.
EDIT: This question use to reference the setContentOffset method inside the block because I want to scroll the collection view in my app. I've removed the method call because peoples' answers focused on why wasn't I using scrollToItemAtIndexPath inside of why contentSize is not being updated.
To get the content size after reload, try to call collectionViewContentSize of the layout object. It works for me.
This worked for me:
[self.collectionView.collectionViewLayout invalidateLayout];
[self.collectionView.collectionViewLayout prepareLayout];
Edit: I've just tested this and indeed, when the data changes, my original solution will crash with the following:
"Invalid update: invalid number of items in section 0. The number of items contained in an existing section after the update (7) must be equal to the number of items contained in that section before the update (100), plus or minus the number of items inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of items moved into or out of that section (0 moved in, 0 moved out)."
The right way to handle this is to calculate the insertions, deletions, and moves whenever the data source changes and to use performBatchUpdates around them when it does. For example, if two items are added to the end of an array which is the data source, this would be the code:
NSArray *indexPaths = #[indexPath1, indexPath2];
[self.collectionView performBatchUpdates:^()
{
[self.collectionView insertItemsAtIndexPaths:indexPaths];
} completion:^(BOOL finished) {
// TODO: Whatever it is you want to do now that you know the contentSize.
}];
Below is the edited solution of my original answer provided by Kernix which I don't think is guaranteed to work.
Try performBatchUpdates:completion: on UICollectionView. You should have access to the updated properties in the completion block. It would look like this:
[self.collectionView reloadData];
[self.collectionView performBatchUpdates:^()
{
} completion:^(BOOL finished) {
// TODO: Whatever it is you want to do now that you know the contentSize.
}];
After calling
[self.collectionView reloadData];
use this:
[self.collectionView setNeedsLayout];
[self.collectionView layoutIfNeeded];
then you can get the real contentSize
Call prepareLayout first, and then you will get correct contentSize:
[self.collectionView.collectionViewLayout prepareLayout];
In my case, I had a custom layout class that inherits UICollectionViewFlowLayout and set it only in the Interface Builder. I accidentally removed the class from the project but Xcode didn't give me any error, and the contentSize of the collection view returned always zero.
I put back the class and it started working again.
Related
I have a UITableViewCell which contains a TWTRTweetView with auto layout. I am loading a tweet like this:
- (void)loadTweetWithId:(NSString *)tweetId {
if (mTweetId == nil || ![mTweetId isEqualToString:tweetId]) {
mTweetId = tweetId;
[[[TWTRAPIClient alloc] init] loadTweetWithID:tweetId completion:^(TWTRTweet *tweet, NSError *error) {
if (tweet) {
NSLog(#"Tweet loaded!");
[mTweetView configureWithTweet:tweet];
[mTweetView setShowActionButtons:YES];
//[mTweetView setDelegate:self];
[mTweetView setPresenterViewController:self.viewController];
[mTweetView setNeedsLayout];
[mTweetView layoutIfNeeded];
[mTweetView layoutSubviews];
hc.constant = mTweetView.frame.size.height;
[self updateConstraints];
[self layoutIfNeeded];
[self layoutSubviews];
[self.tableView setNeedsLayout];
[self.tableView layoutIfNeeded];
[self.tableView layoutSubviews];
} else {
NSLog(#"Tweet load error: %#", [error localizedDescription]);
}
}];
}
}
When tweet loaded cell doesn't resize unless I scroll it out and scroll it to back. I have tried several approaches as you can see in code snippet. But non of these works. My table view uses full auto layout approach which doesn't implement cell height for row function. How can i fix this?
UPDATE:
Using:
[self.tableView beginUpdates];
[self.tableView endUpdates];
is not possible because when I do that all cells being redrawn and very big jumping happens and that is not acceptable. Also I have confirmed that tweet completion block runs in main thread.
UPDATE 2:
I have also tried to cache tweet view with tweet id and reload cell for related index path and give the same tweet view for tweet id. The cell height is corrected but it doesn't become visible until scroll out/in.
UPDATE 3:
I give constraints to tweet view in xib of the cell and height constraint is connected. So this is not a main thread issue. I have also mentioned that reloading particular cell at index doesn't work.
While working an other solution I have seen some sample TwitterKit codes that uses TWTRTweetTableViewCell but was preloading tweets to configure the cells. So I have done the same. This is a workaround of course.
Updated Answer:
You're doing a couple of things wrong that are likely to cause (or at least contribute to) the jumping:
Never call layoutSubviews yourself.
It's a method called by the system to resolve your constraints. It's automatically triggered when calling setNeedsLayout and layoutIfNeeded in a row.
The same applies to updateConstraints. It is called by the system during a layout pass. You can manually trigger it by subsequently calling setNeedsUpdateContraints and updateConstraintsIfNeeded. Furthermore, it only has an effect if you actually implemented (overrode) that method in your custom view (or cell).
When you call layoutIfNeeded on a view it layouts its subviews. Thus, when you change the constant of a constraint that constrains your mTweetView, it probably won't have any effect (unless the view hierarchy is invalidated during the triggered layout pass). You need to call layoutIfNeeded on mTweetView's superview which is the cell's content view (judging from the screenshot you added to your post):
[contentView layoutIfNeeded];
Furthermore, there is one more thing you need to be aware of that can cause flickering as well:
Cells in a table view are being recycled. Each time a cell is reused you load a new tweet. I guess it's from an asynchronous network request? If so, there is the possibility that the completion block from the first tweet you load for that cell instance returns after the completion block from the second tweet you load for that (recycled) cell when you scroll really fast or you internet connection is really slow. Make sure you cancel the request or invalidate it somehow when your cell is reused (prepareForReuse method).
Please make sure you've fixed all these issues and see if animation now works as expected. (My original answer below remains valid.)
Original Answer:
I'm pretty sure that
[self.tableView beginUpdates];
[self.tableView endUpdates];
is the only way to have a cell auto-resize itself while being displayed.
Reason:
For historic and performance reasons a UITableView always works with fixed-height cells (internally). Even when using self-sizing cells by setting an estimatedRowHeight etc. the table view will compute the height of a cell when it's dequeued, i.e. before it appears on screen. It will then add some internal constraints to the cell to give it a fixed width and a fixed height that just match the size computed by Auto Layout.
These internal constraints are only updated when needed, i.e. when a row is reloaded. Now when you add any constraints inside you cell you will "fight" against these internal constraints which have a required priority (aka 1000). In other words: There's no way to win!
The only way to update these internal (fixed) cell constraints is to tell the table view that it should. And as far as I know the only public (documented) API for that is
- (void)beginUpdates;
- (void)endUpdates;
So the only question that remains is:
Why is this approach not an option for you?
I think it's legitimate to redraw a cell after it's been resized. When you expand the cell to show a longer tweet than before the cell needs to be redrawn anyway!
You probably won't (and shouldn't) resize all visible cells all the time. (That would be quite confusing for the user...)
Try reloading that particular cell, after you loaded the tweet using,
- (void)reloadRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths
withRowAnimation:(UITableViewRowAnimation)animation;
I had the similar issue and i got that fixed by adding all my code in dispatch_async to make sure its running on main thread.
dispatch_async(dispatch_get_main_queue(), ^{
/*CODE HERE*/
});
So your code should be like this:
- (void)loadTweetWithId:(NSString *)tweetId {
if (mTweetId == nil || ![mTweetId isEqualToString:tweetId]) {
mTweetId = tweetId;
[[[TWTRAPIClient alloc] init] loadTweetWithID:tweetId completion:^(TWTRTweet *tweet, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (tweet) {
NSLog(#"Tweet loaded!");
[mTweetView configureWithTweet:tweet];
[mTweetView setShowActionButtons:YES];
//[mTweetView setDelegate:self];
[mTweetView setPresenterViewController:self.viewController];
[mTweetView setNeedsLayout];
[mTweetView layoutIfNeeded];
[mTweetView layoutSubviews];
hc.constant = mTweetView.frame.size.height;
[self updateConstraints];
[self layoutIfNeeded];
[self layoutSubviews];
[self.tableView setNeedsLayout];
[self.tableView layoutIfNeeded];
[self.tableView layoutSubviews];
} else {
NSLog(#"Tweet load error: %#", [error localizedDescription]);
}
});
}];
}
}
I try to implement a tableView where rows additional rows are inserted with an animation while the rows are visible. I update the array for the datasource and call:
[self.tableView insertRowsAtIndexPaths:addedIndexPaths withRowAnimation:(UITableViewRowAnimationMiddle)];
I want the tableview to not scroll at all and the rows to be inserted on screen (no over the visible are).
In some section especially the last section of the tableview the cells are inserted the tableview scrolls to completely different section.
I tried different row animation, calling layoutIfNeeded inside of and beginUpdate / endUpdate block, set the correct content offset inside of the beginUpdate / endUpdate block or reload the entire section.
Nothing works. The scrollview always scrolls up.
The cells size them selves using autolayout-constraints. On the top half of the tableview the cells are inserted as I expected.
How can I fix the content Offset while cells are inserted with an animation?
How can I debug that animation?
I faced same issue and after trying a lot found solution that if I remove section header height as UITableViewAutomaticDimension then adding and removing rows from UITableView works perfectly and table view also fixed at same position.
tableView.sectionHeaderHeight = 10;
tableView.estimatedSectionHeaderHeight = UITableViewAutomaticDimension;
remove above 2 lines if added.
tableView.estimatedRowHeight = 15;
tableView.rowHeight = UITableViewAutomaticDimension;*
Row with Automatic Dimension will not create any issues.
But section header view with Automatic Dimension will create issue.
Thanks.
This is a know bug with the UITableViews, it's been there quite a long time and hasn't been fix (by the looks of it, it never will).
The root of this is because your cells are of dynamic sizes and (I'm assuming) you are using the default 'UITableViewAutomaticDimension'. To fix this you need to drop the use of 'UITableViewAutomaticDimension' and calculate the size of each cell. This should help with the bounciness of the table view but probably wont be perfect.
Another way is to rely on CATransaction to lock and keep the table view position after the insert.
It should look something like this (probably, I didn't test it):
CGFloat offset = self.tableView.contentSize.height - self.tableView.contentOffset.y;
[CATransaction begin];
[CATransaction setDisableActions:YES];
[CATransaction setCompletionBlock: ^{
// Code to be executed upon completion
}];
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths: indexPaths
withRowAnimation: UITableViewRowAnimationAutomatic];
self.tableView.contentOffset = CGPointMake(0,
self.tableView.contentSize.height - bottomOffset);
[self.tableView endUpdates];
[CATransaction commit];
I need to reload tableview, but in somehow I want to keep the scroll position. Any way to do it? Maybe store content offset and in viewDidLayoutSubviews I should reset it?
TableView does not reset scroll position. It maintains content offset even after reloadData function is called. So no need to do anything, if you want to keep pixel position.
If you are changing row height at the time of reloading data or changing datasource then you can use scrollToRowAtIndexPath:atScrollPosition:animated: method.
You can save your current offset before reloading the table use this method to save your current offset
self.tableView.contentOffset.y
Or you can Reload like this it will Reload your table from your current Offset
Try this:
[UIView animateWithDuration:0.1 delay:0 options:0 animations:^{
self.tableView.contentOffset = CGPointMake(0, currentOffset);
} completion:^(BOOL finished) {
[self.tableView reloadData];
}];
After reloading the table view you need to call the TableView's method scrollToRowAtIndexPath:atScrollPosition:animated:
Just provide the index path, scroll position which could be one of the following
UITableViewScrollPositionNone,
UITableViewScrollPositionTop,
UITableViewScrollPositionMiddle,
UITableViewScrollPositionBottom
and YES/ NO in animated parameter.
just call scrollToRowAtIndexPath:atScrollPosition:animated: before you use reloadData (with animation:NO)
Also, be carefull in case you have less cells in the updated tableview to prevent crashes.
Hope it helps
I have a tableview with dynamically sized cells, and a button that toggles the sort order of these cells. I'd like to scroll to the top every time the sort order is toggled, but when I set the content offset to the top, it seems to only scroll ~90% of the way there.
The offsetting code is simple enough and has served me well on different projects, so I seriously doubt the problem is here:
- (void) scrollToTop
{
CGPoint offset = CGPointMake(0, -self.tableView.contentInset.top);
[self.tableView setContentOffset:offset animated:YES];
}
[self.tableView reloadData]; // Lets update with whatever info we have
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:NO];
reloading and then scrolling resolved for me.
Didn't think I would find the answer so soon.
I was using UITableView's tableView:estimatedHeightForRowAtIndexPath: to return my minimum cell height, and it seems that the tableview uses this inside reloadData to create an idea of how big the content is before actually dequeuing the cells and caching their height. Being halfway down the content and reloading the data causes the tableview to think the distance to the top is the (number of cells offscreen above the current visible * the minimum height from estimatedHeightForRow), causing the tableview to only offset itself as if all cells were the minimum height. My solution was just to avoid using the estimated height, since my tableview isn't excessively long anyway. If you do have a large tableview (approaching 1000+ rows) that actually needs to use the estimated values for performance reasons, you might want to find a way to make the estimated values as close to the runtime values as possible, or look into more detailed solutions.
tl;dr - Remove tableView:estimatedHeightForRowAtIndexPath: and just allow the tableView to size itself from heightForRowAtIndexPath
What about something like this instead?
NSIndexPath *start = = [NSIndexPath indexPathForRow:0 inSection:0];
[self.tableView scrollToRowAtIndexPath:start atScrollPosition:UITableViewScrollPositionTop animated:YES];
None of these worked for me. The solution was to call layoutIfNeeded() before setting the content offset:
tableView.reloadData();
tableView.layoutIfNeeded();
tableView.setContentOffset(CGPoint.Empty, animated: true);
I have a UICollectionView that gets updated like so:
[self.collectionView performBatchUpdates:^{
[self.collectionView deleteItemsAtIndexPaths:pathsRemoved];
[self.collectionView insertItemsAtIndexPaths:pathsAdded];
}
Usually, this works just fine, but under certain circumstances the data source count doesn't match the original number of items - count of pathsRemoved + count of pathsAdded.
Therefore, I wrapped this code inside a #try/#catch block that then calls [self.collectionView reloadData].
What ends up happening, however, is that after reloading the data, the layout becomes messed up; cells that were previously there (above on-screen cells, but off screen) no longer show. The collection view's content size is correct, and I can scroll up an down, but only see the cells that were on screen at time of reload.
Interestingly, Reveal.app shows that the other cells do in fact exist, but are hidden; I have a hunch that this is a red herring, and that there is some view caching going on here, but the cells are nonetheless listed in Reveal.
I have tried invalidating the layout, calling layoutIfNeeded, as well as the various subclass-of-flow-layout tricks I've found, and nothing has fixed it.
I've resorted to mitigating the issue by doing my own benign assertion, and doing a check to make sure the counts add up. If they do, performBatchUpdates. Else, don't bother, and just reload. This works, since we're bypassing performBatchUpdates: altogether. But I have to ask:
Is there a better way to gracefully recover from an assertion exception thrown from within performBatchUpdates:? Or is it that UICollectionViews are just buggy (still, on iOS 7).
I just find a way to handle this !
I tried like you to wrap the performBatchUpdates: code inside a #try/#catch block that calls [collectionView reloadData].
Then I realized that the problems disappeared when my ViewController was deallocated, the collectionView also.
So I found the ugly solution (but working) : kill your old collectionView and create a new one in the catch block.
#try {
[collectionView performBatchUpdates:^ {
// Do your stuff
}];
}
#catch (NSException *exception) {
UIView *oldSuperview = self.collectionView.superview;
[self.collectionView removeFromSuperview];
self.collectionView = [[UICollectionView alloc] initWithFrame:self.collectionView.frame collectionViewLayout:self.collectionView.collectionViewLayout];
self.collectionView.dataSource = self;
self.collectionView.delegate = self;
// set other collectionView's properties here
[oldSuperview addSubview:self.collectionView];
}
I know this is bad, but it saved me !