I'm currently updating an app to iOS8, and am replacing my own cell height calculations. I have a tableview with a bunch of custom cells. Each cell when selected will present/push a new view onto the navigationController. When the tableview is populated with these cells, and the user selects one near the bottom of the table, the tableview jumps to the top right before the new view is presented. This is very distracting to the user. I'm trying using self sizing cells introduced in iOS 8 here (in the viewDidLoad):
self.tableView.estimatedRowHeight = 60.0;
self.tableView.rowHeight = UITableViewAutomaticDimension;
Here's the code for when a cell is selected
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UIViewController *selectedViewController = [[UIStoryboard storyboardWithName:#"Trip" bundle:nil] instantiateViewControllerWithIdentifier:#"TripDetail"];
DetailViewController *destViewController = (id)selectedViewController;
[destViewController setData:self.dataStore];
[self.navigationController pushViewController:selectedViewController animated:YES];
}
Does anyone know why this is happening? And how I can make it stop?
It's a known issue. The tableView is calling its delegate to get an estimated height for the rows, which is causing the table to scroll.
You can wait to see if it's fixed in the next update, or implement tableView:estimatedHeightForRowAtIndexPath: (as a workaround for now) and return a cached height. Since the estimate will be the actual height, the table won't jump.
This happen's when the table delegate was tapped "didSelectRowAtIndexPath:"
because the delegate method "numbersOfSectionsInTableView" && "numbersOfRowsInSection" will get called. its like reloading the table.
You can fix that jumping state with this method "heightForRowAtIndexPath:" however you'll have to disregard your auto sizing.
Related
Here's how I set my table:
self.tableView = [[UITableView alloc] initWithFrame:self.view.bounds];
self.tableView.delegate = self;
self.tableView.dataSource = self;
self.tableView.estimatedRowHeight = 150;
[self.view addSubview:self.tableView];
Within my cells, I call - (CGSize)sizeThatFits:(CGSize)size to programmatically return the height (which is set in - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath. I'm not using constraints or autolayout.
Nothing complex and my heights are all perfectly laid out visually.
However the problem is when I push a new view controller, the cells jump/shift visually (either up or down). It seems to be jumping based on calculating the estimated row height values - yet sizeThatFits is also called for each visible cell before shifting so I'm really confused (not sure why either needs to be called at all really, since I'm leaving the view). I've checked the contentOffset for the tableView - it's unchanged so it's not the problem.
Okay, I solved it by caching my cell heights in sizeThatFits, and returning that value for estimated cell heights within the delegate. Works beautifully.
Quick fix:
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
self.tableView.reloadData()
}
Edit: After spending hours on this and similar issues I've found the best solution is to cache the cell heights and return them in estimatedHeightForRowAtIndexPath delegate method, the problem is caused by estimated heights being really inaccurate.
I cached the heights in tableView(_:willDisplayCell:forRowAtIndexPath:) into a dictionary with the unique ID's for the data as keys this way when data gets added or updated I can just remove that height from the cache and use better estimated heights so only that cell uses an estimated height. So far this solves all my jumping and scrolling issues.
I have a tableview that auto scrolls only in iOS 8 when I open a new view using [self.navigationController pushViewController:newViewController animated:YES];
Detailed Problem :
Now I'm going to give a detail of how I'm getting the problem, it's particularly in iOS 8. Take any tableview having around 50 entries, scroll it down to make the 10th entry at the top of the tableview. Then select any item on the tableview. Use the below method to push a view controller on row selection in tableview.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[tableView deselectRowAtIndexPath:indexPath animated:YES];
// Open the New View
NewViewController *newVC = [[NewViewController alloc] init];
[self.navigationController pushViewController:newVC animated:YES];
}
Now you will find that on coming back from NewViewController to the previous ViewController, the tableview autoscrolls some distance. The tableview doesn't stay at the place of scroll it always changes its position automatically.
I had the same problem. I was using the new iOS 8 feature dynamic cell heigh. I was setting:
self.tableView.estimatedRowHeight = 50.0;
self.tableView.rowHeight = UITableViewAutomaticDimension;
The problem was that some cells were much higher than 50. Solution was to provide delegate method estimatedHeightForRowAtIndexPath and return values that are closer to the actual cell height. For example:
-(CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row == 1) {
return 300.0;
}
return 50;
}
Another solution is to calculate cell heigh using systemLayoutSizeFittingSize inside cellForRowAtIndexPath and cache that value. Inside estimatedHeightForRowAtIndexPath supply cached values but return default if the cell height wasn't cached yet.
Just remove self.tableView.estimatedRowHeight = xxx; It works for me . The problem happens in iOS8 and does not appear in iOS10
Consider an standard, vertically scrolling flow layout populated with enough cells to cause scrolling. When scrolled to the bottom, if you delete an item such that the content size of the collection view must shrink to accommodate the new number of items (i.e. delete the last item on the bottom row), the row of cells that scroll in from the top are hidden. At the end of the deletion animation, the top row appears without animation - it's a very unpleasant effect.
In slow motion:
It's really simple to reproduce:
Create a new single view project and change the default ViewController to be a subclass of UICollectionViewController
Add a UICollectionViewController to the storyboard that uses a standard flow layout, and change its class to ViewController. Give the cell prototype the identifier "Cell" and a size of 200x200.
Add the following code to ViewController.m:
#interface ViewController ()
#property(nonatomic, assign) NSInteger numberOfItems;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.numberOfItems = 19;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return self.numberOfItems;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
return [collectionView dequeueReusableCellWithReuseIdentifier:#"Cell" forIndexPath:indexPath];
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
self.numberOfItems--;
[collectionView deleteItemsAtIndexPaths:#[indexPath]];
}
#end
Additional Info
I've seen other manifestations of this problem when dealing with collection views, it's just that the above example seems the simplest to demonstrate the issue. UICollectionView seems to go into some kind of paralysed state of panic during the default animations, and refuses to unhide certain cells until after the animation completes. It even prevents manual calls to cell.hidden = NO on hidden cells from having an effect (hidden is still YES afterwards). Dropping down to the underlying layer and setting hidden there works, provided you can get a reference to the cell you want to unhide, which is non-trivial when dealing with cells that haven't been displayed yet.
-initialLayoutAttributesForAppearingItemAtIndexPath is being called for every item visible at the time of the call to deleteItemsAtIndexPaths:, but not for the ones that are scrolled into view. It is possible work around the issue by calling reloadData inside a batch update block immediately afterwards, which appears to make the collection view realise that the top row is about to appear:
[collectionView deleteItemsAtIndexPaths:#[indexPath]];
[collectionView performBatchUpdates:^{
[collectionView reloadData];
} completion:nil];
But unfortunately this is not an option for me. I am trying to implement some custom animation timing by manipulating the cell layers & animations, and calling reloadData really throws things out of whack by causing unnecessary layout callbacks.
Update: A bit of investigation
I added log statements to a lot of layout methods and looked through some stack frames to try and find out what's going wrong. Crucially, I'm checking when layoutSubviews is called, when the collection view asks for layout attributes from the layout object (layoutAttributesForElementsInRect:) and when applyLayoutAttributes: is called on the cells.
I would expect to see a sequence of methods like this:
// user taps cell (to delete it)
-deleteItemsAtIndexPaths:
-layoutAttributesForElementsInRect:
-finalLayoutAttributes...: // Called for the item being deleted
-finalLayoutAttributes...: // \__ Called for each index path visible
-initialLayoutAttributes...: // / when deletion started
-applyLayoutAttributes: // Called for the item being deleted, to apply final layout attributes
// collection view begins scrolling up
-layoutSubviews: // Called multiple times as the
-layoutAttributesForElementsInRect: // collection view scrolls
// ... for any new set of
// ... attributes returned:
-collectionView:cellForItemAtIndexPath:
-applyLayoutAttributes: // Sets the standard attributes for the new cell
// collection view finishes scrolling
Most of this is happening; layout is correctly triggered as the view scrolls, and the collection view properly queries the layout for the attributes of cells to be displayed. However, collectionView:cellForItemAtIndexPath: and the corresponding applyLayoutAttributes: methods are not being called until after the deletion, when layout is invoked one last time causing the hidden cells to be assigned their layout attributes (sets hidden = NO).
So it seems that despite receiving all the correct responses from the layout object, the collection view has some kind of flag set to not update the cells during the update. There is a private method on UICollectionView called from within layoutSubviews that seems responsible for refreshing the cells' appearance: _updateVisibleCellsNow:. This is from where the data source eventually gets asked for a new cell before applying the cells starting attributes, and it seems this is the point of failure, as it is not being called when it should be.
Additionally, this does seem to be related to the update animation, or at least cells are not updated for the duration of the insertion/deletion. For example the following works without glitches:
- (void)addCell
{
NSIndexPath *indexPathToInsert = [NSIndexPath indexPathForItem:self.numberOfItems
inSection:0];
self.numberOfItems++;
[self.collectionView insertItemsAtIndexPaths:#[indexPathToInsert]];
[self.collectionView scrollToItemAtIndexPath:indexPathToInsert
atScrollPosition:UICollectionViewScrollPositionCenteredVertically
animated:YES];
}
If the above method is called to insert a cell while the inserted cell is outside the current visible bounds, the item is inserted without animation and the collection view scrolls to it, properly dequeuing and displaying cells on the way.
Problem occurs in iOS 7 & iOS 8 beta 5.
Adjust your content insets so that they go beyond the bounds of the device's screen size slightly.
collectionView.contentInsets = UIEdgeInsetsMake(-5,0,0,0); //Adjust this value until it looks ok
I have custom cell on my table view which I've created with nib file. I have there 2 horizontal scrollViews. Target platform is iOS 7.
On Cell XIB file I've created "constrants" for every tableView (because I'm using autolayout).
On my Custom cell implementation file I have such code:
-(void)layoutSubviews
{
self.SwitchPageSV.contentSize = CGSizeMake(960, 240);
self.SwitchPageSV.scrollEnabled = YES;
self.SwitchPageSV.delegate = self;
self.ButtonsSV.contentSize = CGSizeMake(400, 40);
self.SwitchPageSV.scrollEnabled = YES;
}
And it is working like a charm. But when I go from this table view to other View Controller and go back, scroll view does not work anymore. I have to scroll my tableView to recreate cell and than it is working again.
Reloading data doesn't help:
I was trying to define those values on:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
on my viewController, where I'm putting all data to this cell. But from there, my settings for scroll view are not working at all. I have no idea what to do with that thing. Any help?
This is because layoutSubviews method in your code only invokes after initialization. But you need to execute the code inside this method even after coming back. For this you have to call setNeedsLayout
[self setNeedsLayout ];
I have a UITableview cell that gets a tally from a core data database. The tallyTable is in a view controller inside a UITab view. I have an NSLog statement that prints out the tally value whenever it gets updated. Another tab has a list to change the source (different day) for the tallies. I am using iOS5 with ARC targeting iOS 4.2.
Here's the problem. When I load the application, the correct tallies for whatever the last selected day show up in the table tab. If I then go to the day tab and change the day and return to the tally tab there is no change in the display. However, the viewWillAppear on the tally tab runs and as the table cycles through cellForIndexPath, my NSLog statement prints out all the correct new values. If I then scroll the top label off the screen and back the label updates to the new value.
I've tried setNeedsLayout and setNeedsDisplay on the UILabel, the UITableViewCell, the UITableView and the view controller loading the table. I tried changing the CellReuse identifier so that it would never reuse a cell.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
CollectionItemTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[CollectionItemTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
NSUInteger row = [indexPath row];
cell.textLabel.text = [[self.collectionKeys objectAtIndex:row] valueForKey:#"collectionTitle"];
NSInteger test1 = indexPath.row + 150;
NSLog(#"tag = %i", test1);
cell.tallyButton.tag = test1;
NSNumber * questionID = [[self.collectionKeys objectAtIndex:row] valueForKey:#"answerID"];
cell.tallyLabel.text = [NSString stringWithFormat:#"%i",[self updatePointTotal:questionID]];
NSLog(#"Collection text should be = %#", [NSString stringWithFormat:#"%i",[self updatePointTotal:questionID]]);
[cell setNeedsLayout];
return cell;
}
I've read over a half dozen other similar questions. Got about three hours invested so far in trying to solve this.
EDIT: I thought I fixed it by using the navigation controller to repush the top level view controller on to the view again. I'll admit now this feels like a classically kludgy hack in every way. When the view is PUSHED everything updates and it is seamless. However, in order to have a fixed footer to make selection settings for the table buttons, I used a UIView with two subviews, a UITableView on top and a simple UIView with four buttons below.
The captions on the buttons need to change with the data source. Now when the view controller is pushed onto the view it obscures my fixed footer view. So, I inserted the fixed footer into the UITableview and everything appeared fine until I scrolled the UITableView and the footer scrolled up with it. The table is basically a tally sheet with buttons next to each item and in the footer is four buttons to note the color of the tallied item. Say the next item was a green lego, you would tap "green" in the footer and the button next to "lego" in the table. When I push the view controller with the two subviews the UITableview labels do not update. Thus the tableview needs to be pushed itself (as far as I can tell).
ANSWER: see comment below but ultimately I needed to reload both the visible UITableView data and the delegate UITableView controller data behind it.
I'll give it a shot. First, are you using ARC? If not, you need to add autorelease when you alloc/init a new cell. Otherwise, it's fine as is.
If I'm understanding your question correctly:
The tableView displays the correct data at app launch
You switch away from the tab with the tableView and change the tableView dataSource
You switch back to the tab with the tableView and you see (via NSLog) that the table cells are reloaded with the correct data yet the old data is still visible in the cells
If you scroll a cell off the display and back forcing it to refresh it contains the correct data
Some thoughts:
the tableView will not reload itself automatically when it's view appears. You need to call [tableView reloadData] whenever the dataSource changes. This is independent of whether the tableView is currently displayed or not. My guess is this alone will solve your problem.
You don't need to call setNeedsLayout on the cell unless you want the cell to relayout its subviews based on the data. You also don't need setNeedsDisplay.
I'm assuming there aren't other complicating factors (such as multiple tableViews displaying the same data) that could confuse things.
If you use prepare for reuse method, remember to over the original method with [super prepareForReuse];
Another method if the above way does not work is re setup cell as describe here.
I use the same method i applied for some of my collection view : we should remove/reset your subview where you create/add it to cell 's content. That mean we need set up data each cell completely for each row.
I move the code reset data value from prepare reuse to where i set value and I worked simply !
In my CustomCell.m :
- (void)configCellWith:(id)item At:(NSUInteger)row {
if (_scrollView) {
[[_scrollView subviews]
makeObjectsPerformSelector:#selector(removeFromSuperview)];
_scrollView = nil;
[_scrollView removeFromSuperview];
}
else {
CGFloat y = labelHeight+15;
float scrollHeight = _imgArray.count*200;
_scrollView=[[UIScrollView alloc]initWithFrame:CGRectMake(10, y,SCREEN_WIDTH-20, scrollHeight)];
_scrollView.scrollEnabled=YES;
_scrollView.userInteractionEnabled=YES;
_scrollView.layer.masksToBounds = YES;
[self.contentView addSubview:_scrollView]; } }
Remember to change your data source appropriately too.