I have a table view with custom cells (all configured in a subclass using auto layout).
The cells load fine, display fine, everything is fine.
The issue is when I am inserting more rows (at the bottom). The table view is representing a feed for posts, so when the user scrolls to the bottom, before reaching the last cell, I load new posts, and then insert them into the table.
When I do this, I get this weird glitchy effect where the cells randomly come down (behind the previous cells) into place, the table view scrolls up a bit, messy.
CODE AT BOTTOM
I've uploaded a clip of me scrolling. When you see the activity indicator,
I stop scrolling. The rest of the movement is from the glitchy behavior.
Is the reason for the glitch because the cells are being drawn with auto-layout?
I would hope not, but idk..I'm not sure what to do regarding a solution. If anyone has any ideas please let me know.
FYI:
I have this (of course, since the cells are all using auto layout)
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewAutomaticDimension;
}
I've tried setting the estimated height to an "average" of the expected cell heights, around 65. No difference.
Update
Here's some code:
HomeViewController.m --> viewDidLoad
...
self.tableView = [KATableView.alloc initWithFrame:CGRectZero style:UITableViewStylePlain];
self.tableView.delegate = self;
self.tableView.dataSource = self;
self.tableView.refreshDelegate = self;
self.tableView.estimatedRowHeight = 75;
self.tableView.rowHeight = UITableViewAutomaticDimension;
[self.view addSubview:self.tableView];
// Constrains to all 4 sides of self.view
[SSLayerEffects constrainView:self.tableView toAllSidesOfView:self.view];
my table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (!self.dataManager.didFinishFetchingData) return 4;
if (self.contentObjects.count == 0) return 1;
if (self.dataManager.moreToLoad) return self.contentObjects.count + 1;
return self.contentObjects.count + 1;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewAutomaticDimension;
}
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewAutomaticDimension;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MYObject *object = self.contentObjects[indexPath.row];
SomeTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:object.documentID];
if (!cell) {
cell = [SomeTableViewCell.alloc initWithStyle:UITableViewCellStyleDefault reuseIdentifier:object.documentID];
cell.delegate = self;
} else [cell startListeningForChanges];
return cell;
}
Here is how I am loading more data and adding it to the table view..
- (void)getHomeFeedData:(nullable void(^)(BOOL finished))completed {
[self.dataManager fetchHomeFeedDataForFeedOption:self.homeNavController.feedFilterOption completion:^(NSError * _Nullable error, NSArray<__kindof KAObject *> * _Nullable feedObjects) {
if (error != nil) {
NSLog(#"something went wrong: %#", error.localizedDescription);
if (completed) completed(NO);
return;
}
NSInteger originalCount = self.contentObjects.count;
if (self.dataManager.isFirstTimeLoading) self.contentObjects = feedObjects.mutableCopy;
else {
if (self.dataManager.isGettingNew) for (MYObject *obj in feedObjects) [self.contentObjects insertObject:obj atIndex:0];
else if (feedObjects.count > 0) [self.contentObjects addObjectsFromArray:feedObjects];
}
if (feedObjects.count > 0) {
if (self.dataManager.isFirstTimeLoading) [self.tableView reloadData];
else {
[self.tableView insertCells:feedObjects forSection:0 startingIndex:self.dataManager.isGettingNew? 0 : originalCount];
}
} else if (self.dataManager.isFirstTimeLoading) [self.tableView reloadData];
if (completed) completed(YES);
}];
}
NOTE:
[self.tableView insertCells:feedObjects forSection:0 startingIndex:self.dataManager.isGettingNew? 0 : originalCount];
is simply this:
- (void)insertCells:(nullable NSArray *)cells forSection:(NSInteger)section startingIndex:(NSInteger)start {
if (!cells) return;
NSMutableArray *indexPaths = #[].mutableCopy;
for (id obj in cells) {
NSInteger index = [cells indexOfObject:obj] + start;
[indexPaths addObject:[NSIndexPath indexPathForRow:index inSection:section]];
}
[self insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade];
}
Update 2
My UITableViewCell subclass content is hidden ATM (too much difficulty in editing all my post content for the purpose of this post). I just have the subviews of each cell set to alpha = 0.f. It's just an image view, some labels, and some buttons.
No constraint issues in console, cells render perfectly when calling [self.tableView reloadData] so maybe there is something I'm doing wrong when inserting the cells?...
When you dealing with UITableView glitches:
Make sure you call UIKit API's on a main thread - turn on Main Thread checker
In your case, there might be an issue that fetchHomeFeedDataForFeedOption:completion: completion block is called not on a main thread.
Your insert is definitely wrong - all delete/insert/update/move calls for UITableView should be wrapped in beginUpdates/endUpdates
Your "load more" component at the bottom might be an issue. You need to address how it's managing contentSize/contentOffset/contentInset of table view. If it does anything but manipulating contentInset - it does wrong job.
While it's hard without debugging the whole solution, I bet options 2 & 3 are the key problems out there.
Related
I have created expanded tableview using plist, I have populated all the values and images into my tableview. Here I want to make animation for my particular cell when user click that cell.
For example, first time images will be default, next time if I click that cell, the particular cell get expanded. Here I need to make my image has hide; again if user press it will show the image. How to do this?
Every time the user taps a cell, a UITableViewDelegate method gets called, and in this method you can reload the cell:
- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
When a cell is reloaded, the method below is called (between the others), and the only thing you have to do is to return a different height according with the current state of the cell:
- (CGFloat)tableView:(UITableView *)tableView
heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return (/* cell state */) ? 100.0 : 50.0;
}
You have multiple ways to store your cell's state, you can do it in its model for example.
There are other ways also, this is only a solution. But the key point is that you have to reload the cell, and return a different height according with the conditions you want to consider.
You can check my full solution in the following github
Here is some of my implementation.
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
movies = [[NSArray alloc] initWithObjects:
(NSArray*)[[Section alloc ] init:#"Istanbul" movieNames:#[#"Uskudar", #"Sariyer"] isExpanded:false],
(NSArray*)[[Section alloc ] init:#"Bursa" movieNames:#[#"Osmangazi", #"Mudanya", #"Nilufer"]
isExpanded:false],
(NSArray*)[[Section alloc ] init:#"Antalya" movieNames:#[#"Alanya", #"Kas"] isExpanded:false], nil
];
}
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return movies.count;
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return ((Section*)movies[section]).movies.count ;
}
-(void)toggleSection:(ExpandableHeaderFoorterView *)headerView withSection:(int)section
{
((Section*)movies[section]).expanded = !((Section*)movies[section]).expanded;
[expandableTableView beginUpdates];
for (int i= 0; i< ((Section*)movies[section]).movies.count; i++)
{
NSArray* rowsToReload = [NSArray arrayWithObjects:[NSIndexPath indexPathForRow:i inSection:section], nil];
[expandableTableView reloadRowsAtIndexPaths:rowsToReload
withRowAnimation:UITableViewRowAnimationFade];
}
[expandableTableView endUpdates];
}
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
ExpandableHeaderFoorterView* headerView = [[ExpandableHeaderFoorterView alloc] initWithCustom:((Section*)movies[section]).genre withSection:(int)section withDelegate:self];
return (ExpandableHeaderFoorterView*)headerView;
}
http://www.iostute.com/2015/04/expandable-and-collapsable-tableview.html
You can see this tutorial. It's a very good tutorial based on expandable tableview cell.
I'm trying to insert new rows from my Core Data on the bottom of the tableview, everything works fine, except that my tableview scroll a random rows to the top
My table view has variable height (each rows has different height)
I want that when the user scroll on the bottom of the table view, it load 10 more rows from core data but the scroll must stay on the same
my code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row == [_data count] - 1 && !loadingData) {
loadingData = YES;
[self loadMoreData];
}
//Create Cell
}
-(void)loadMoreData {
int from = (int)[_data count];
NSArray *temp = [Functions loadNewDataFrom:from];
if ([temp count] > 0) {
[_data addObjectsFromArray:temp];
[self.tableView reloadData];
loadingData = NO;
}
}
What I'm doing wrong?
Edit: I already try allot of solutions on google & stackoverflow and always its the same,
and I'm trying it on iOS 7 simulator 4 inch
Thanks!
I have a table view which display the contacts from array. I setup the table view delegates by follows.
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [contactArray count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *simpleTableIdentifier = #"ContactCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:simpleTableIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:simpleTableIdentifier];
}
cell.textLabel.text = [contactArray objectAtIndex:indexPath.row];
return cell;
}
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return 60.0;
}
But the first cell in the table view always empty. It starts display only from second cell. I thought it may be header view. So I removed the header using the following delegate methods.
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
return 0.0;
}
But still have the problem. I attached the screenshot about this issue.
Any help will be appreciated.
Your TableView is fine and it is working correctly, this is due to some other problem that is included in iOS 7, that automatically scroll insets. To solve this problem, go to your storyboard and select the viewcontroller in which your TableView is and select the ViewController and select the Properties of that ViewController, and uncheck this checkbox, which is read as Adjust ScrollView Insets. See this screen shot,
Your table is correct.Just your table was auto adjusted by the viewController.
You can write self.automaticallyAdjustsScrollViewInsets = NO;
Your Deduction is wrong your first cell isn't missing, but your tableview has started by 64 points down. So change your frame of your tableview or your tableview constraints accordingly.
Tip : Try setting a background colour when you have to debug things like this to clear your doubts.
I have a table view form created using Static Cells in IB/Storyboard. However, I need to hide some of the cells at runtime depending on certain conditions.
I have found a few 'answers; to this question on SO, e.g.
UITableView set to static cells. Is it possible to hide some of the cells programmatically?
.. and they focus on setting the height of the cell / row to 0. This is great, except I now get exceptions from AutoLayout because the constraints can't be satisfied. How do I get around this last problem? Can I temporarily disable Auto-Layout for a subview? Is there a better way to be doing this in iOS7?
I found the best way to do this is to simply handle the numberOfRowsInSection, cellForRowAtIndexPath and heightForRowAtIndexPath to selectively drop certain rows. Here's a 'hardcoded' example for my scenario, you could do something a little smarter to intelligently remove certain cells rather than hard code it like this, but this was easiest for my simple scenario.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [super tableView:tableView cellForRowAtIndexPath:indexPath];
if (indexPath.section == 0 && hideStuff) {
cell = self.cellIWantToShow;
}
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
CGFloat height = [super tableView:tableView heightForRowAtIndexPath:indexPath];
if (indexPath.section == 0 && hideStuff) {
height = [super tableView:tableView heightForRowAtIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]];
}
return height;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSInteger count = [super tableView:tableView numberOfRowsInSection:section];
if (section == 0 && hideStuff) {
count -= hiddenCells.count;
}
return count;
}
Hide the cells on the storyboard and set the height to 0:
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
let cell: UITableViewCell = super.tableView(tableView, cellForRowAtIndexPath:indexPath)
return cell.hidden ? 0 : super.tableView(tableView, heightForRowAtIndexPath:indexPath)
}
}
If you ensure there's no constraint touching the bottom edge of the cell, autolayout shouldn't barf (tested on iOS 6.0, 6.1, 7.0). You'll still 'anchor' to the top edge and have to pin the heights. (You can do the reverse and anchor to the bottom, of course.)
If your layout depends on both the top and bottom edge positions, it may be possible to programmatically remove the constraints (they're just objects, after all).
The simplest way is to change height of sections and rows. It works for me.
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
if (section == 3) {
return 0;
} else {
return [super tableView:tableView heightForHeaderInSection:section];
}
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [super tableView:tableView cellForRowAtIndexPath:indexPath];
if (indexPath.section == 3) {
cell.hidden = YES;
return 0;
} else {
cell.hidden = NO;
return [super tableView:tableView heightForRowAtIndexPath:indexPath];
}
}
The kosher way to do this is to use dynamic cells, setting the row height to 0 is a hack. Static cells are very convenient but limited in functionality.
I managed to avoid exceptions from Auto Layout by first removing the constraints on the cell's contentView programmatically in viewDidLoad, and then setting that cell's height to 0 in heightForRowAtIndexPath.
I have found a way that allows you even row animations and is working on iOS 8.3. All you need is to implement the tableView:numberOfRowsInSection: data source method and then add/delete row by UITableView methods
insertRowsAtIndexPaths:withRowAnimation: and deleteRowsAtIndexPaths:withRowAnimation:.
Here is example of hiding exact row based on UISwitch state:
- (IBAction)alowNotif:(id)sender {
UISwitch *sw = (UISwitch*)sender;
NSIndexPath *index = [NSIndexPath indexPathForRow:5 inSection:0];
if ([sw isOn]) {
[self.tableView insertRowsAtIndexPaths:#[index] withRowAnimation:UITableViewRowAnimationAutomatic];
}
else {
[self.tableView deleteRowsAtIndexPaths:#[index] withRowAnimation:UITableViewRowAnimationAutomatic];
}
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (![notifications isOn]) {
return 5;
}
return 6;
}
As was mentioned above by #algal, numberOfRowInSection: is still UITableViewDataSource method, so one does not simply know how long its gonna work.
The best way for me was to modify numberOfRowsInSection method. I removed datasource which i did not want to display. Best solution for me, because everything is in one function.
(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if(section==0) {
NSUInteger productCount = _products.count;
for(loop trough your data) {
if(your condition is true)
productCount--;
}
}
return productCount;
} else
return self.groups.count;
}
Implemented this too, for a tableview in a StoryBoard. My cells are embedded in sections with a header, represented by that blue cube in xcode6 IB. Indeed if you implement heightForHeaderInSection, heightForFooterInSection and titleForHeaderInSection,titleForFooterInSection you can access the headers when the table is displayed, and return 0.0 and nil respectively, and return 0 for numberOfRowsInSection.
Basically it all works fine, except that for every cell hidden a ca. 10 pixel high vertical space remains for every cell (section) you hide. Any idea what that could be?
I currently am working with a uitableview that holds mostly standard size cells at 44pts. However, there are a couple that are larger, about 160pts.
In this instance, there are 2 rows at 44pts height, with the larger 160pts row being inserted below, at index 2 in the section.
Removal call:
- (void)removeRowInSection:(TableViewSection *)section atIndex:(NSUInteger)index {
NSUInteger sectionIndex = [self.sections indexOfObject:section];
NSIndexPath *removalPath = [NSIndexPath indexPathForRow:index inSection:sectionIndex];
[self.tableView beginUpdates];
[self.tableView deleteRowsAtIndexPaths:#[removalPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView endUpdates];
}
Delegate call:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
TableViewSection *section = [self sectionAtIndex:indexPath.section];
return [section heightForRowAtIndex:indexPath.row];
}
section call:
- (NSInteger)heightForRowAtIndex:(NSInteger)index {
StandardCell *cell = (StandardCell *)[self.list objectAtIndex:index];
return cell.height;
}
cell call:
- (CGFloat)height {
return 160;
}
What has me confused is when I remove the larger rows from the table, they start to animate, moving underneath the row above. But when they get to a certain point, about a 1/4 of the way through the animation, they disappear instead of finishing the animation.
It seems like the table animates the row with the notion that it's only 44pts, then once it's reached the point where 44pts are underneath the row above, it gets removed from the table. What detail have I overlooked that will give the table the correct notion to automatically animate the row removal?
Thanks for your help.
Update:
I tried commenting out the height function above (which overrides the default that returns 44). This results in a proper animation with no skips. FWIW
One way to solve this is to animate the row height down to 44 just before deleting:
//mark index paths being deleted and trigger `contentSize` update
self.indexPathsBeingDeleted = [NSMutableArray arrayWithArray:#[indexPath]];
[tableView beginUpdates];
[tableView endUpdates];
//delete row
[self.tableView deleteRowsAtIndexPaths:#[removalPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView endUpdates];
And then in your heightForRowAtIndexPath:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if ([self.indexPathsBeingDeleted containsObject:indexPath]) {
//return normal height if cell is being deleted
[self.indexPathsBeingDeleted removeObject:indexPath];
return 44;
}
if (<test for tall row>) {
return 160;
}
return 44;
}
There's a little bit of bookkeeping going on to keep track of index paths being deleted. There are probably cleaner ways to do this. This is just the first thing that came to mind. Here's a working sample project.