Related
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.
I am building a chat application in which I have to to scroll tableview to bottom in order to show the most recent message.
What I am doing is, I am scrolling it to visible rect using the following code:
- (void)tableViewScrollToBottomAnimated:(BOOL)animated {
// return;
if (!_tableViewShouldScroll) {
return;
}
NSInteger numberOfSections = [self.tableView numberOfSections];
if (numberOfSections > 0) {
NSInteger numberOfRows = [self.tableView numberOfRowsInSection:numberOfSections - 1];
if (numberOfRows > 0) {
CGRect footer = [self.tableView rectForFooterInSection:numberOfSections - 1];
[self.tableView scrollRectToVisible:footer animated:animated];
}
}
_tableViewShouldScroll = !_tableViewShouldScroll;
}
It scrolls down pretty well but freezes for a second while going through the last cell when the view is loaded for the first time. On successive calls, this method is working fine.
PS. I am calling the scrolling method in viewdidappear.
Any help here will be appreciated.
I was calling the tableViewScrollToBottomAnimate in viewDidAppear, which was causing the freeze for a fraction of second. I called the method within viewWillLayoutSubviews and it worked like a charm.
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
[self tableViewScrollToBottomAnimated:NO];
}
My UI structure is as follow:
UITabBarController (TBC) -> UINavigationController (NC) -> UITableViewController (TVC)
(for the simplicity of the example lets say the TBC has only one controller on its viewControllers array - the NC)
My TVC has UISearchBar as its table header, and when the TVC appear I hide the search bar beneath the NC navigation bar by settings the table view content offset.
When user tap a cell in the TVC another view controller is pushed (VC) and hides the tab bar with VC.hidesBottomBarWhenPushed = YES;
Now there is a very annoying behavior that I dont know how to solve:
When the user tap the back button from VC back to TVC, the search bar jumps to be visible even if it was hidden (beneath the navigation bar) before the VC was pushed.
This effect happens only if the TVC doesn't have enough rows to fill the screen, its like the search bar force itself to be visible if there is a place on screen. but its really looks bad and buggy.
I uploaded a simple project that demonstrates the problem, it has the same structure as I described in my question.
I added two bar buttons for your convenience, the "hide bar" button hides the search bar for you, and the "toggle count" button toggle the table view rows count to demonstrate that the issue happens only if there are few items.
Okay.. It looks to me like you've stumbled upon a bug. It should be reported through apples bugreporter (here).
I've made a fairy simple working work-around, but keep in mind that it is a work-around. This will work, but you might have to review it if you have/add other controls to the tableView. It should be safe to use(not acting randomly), and it's not the ugliest of work-arounds, so I think it's fine to use in a release. I've uploaded the same project with the fix here, and you can just go ahead and download it, and you'll probably understand what I've done. I'll explain (in extreme detail) what I've actually thought and done here, in case the download links dies in the future:
Train of thought:
As simalone also said, the problem is that when hidesBottomBarWhenPushed is set to YES, then it will call an additional viewDidLayoutSubviews which somehow resets your current state. We need to override viewDidLayoutSubviews, and check if we are laying out subviews because we are coming from ViewController, or if it's just a regular call. When we establish that the call is indeed because we are returning from ViewController, we need to hide the search bar (only if it was hidden before).
When we return from ViewController, three calls are made to viewDidLayoutSubviews in TableViewController. I'm guessing the first is for tableView, and it seems that the second call is 'for'(or rather from) the tabBar. This second one is the one moving the searchBar down. I have no idea what the third call is, but we can ignore it.
So now there are three things we need to check inside viewDidLayoutSubviews: We need to check if we are returning from ViewController, we need to check if the searchBar was hidden before we pushed(if it should hidden be now), and we need to check that it's the second call to this method.
First things first.
In TableViewController, I added a property #property BOOL backPush; to the header(.h)-file. Now I need to change this variable from ViewController.
In ViewController, I put this:
#import "TableViewController"
...
-(void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
if(self.isMovingFromParentViewController)
{
if([self.navigationController.topViewController isKindOfClass:[TableViewController class]])
[((TableViewController*)self.navigationController.topViewController) setBackPush:YES];
}
}
In the code above, when the view is disappearing (I.E pushing forward, back, closing, whatever), I'm checking if we are disappearing because it was removed from the parent. If it is(which it is when the back-button is called), I check if the now-current top view controller is of class TableViewController, which it also is if we go back. Then I set the property backPush to YES. That's the only thing we need in ViewController.
Now, to the TableViewController. I added a counter next to your row-count:
#interface TableViewController () {
NSInteger _rows;
int count;
}
This is to keep track of how many calls have been made to viewDidLayoutSubviews later. I set count = 0; in viewDidLoad.
Now to the magic:
-(void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
if((self.backPush && count == 0 && self.tableView.contentOffset.y ==
self.tableView.tableHeaderView.frame.size.height) ||
(self.backPush && count == 1 &&
self.tableView.contentOffset.y == 0))
{
if(count == 0)
count++;
else
{
count = 0;
self.backPush = NO;
[self hideSearchBar];
}
}
else if((count == 0 || count == 1) || self.tableView.tableHeaderView.isFirstResponder)
{
count = 0;
self.backPush = NO;
}
}
The first if-statement wants either of these situations:
backPush is YES, count is 0, and searchBar is already hidden.
backPush is YES, count is 1, and searchBar is visible.
If 1. is true, then we increment count by 1.
If 2. is true, then 1. has already happened, and we now know that we are in the second round of viewDidLayout.. when we are coming back from VC AND that the searchBar WAS hidden (because 1. happened) but now isn't hidden. It probably happens in the super-method or something.
Now we can finally push the searchBar out again. I also reset count and set backPush back to NO.
The else if is also pretty important. It checks if count is 0 or 1, or if the searchBar has the keyboard showing. If count is 0 or 1 when it reaches here, it means that the first if-statement failed, e.g that the searchBar wasn't hidden, or that it was scrolled far up.
(When I think of it, the else-if should check if backPush is YES as well. Now it sets those variables repeatedly)
Let me know if you find a better way!
I think this one is simple solution. Thanks to
Sti
for giving some ideas to solve this bug.
Initialize variable var hideSearchBar = false
and inside viewDidLayoutSubviews add this code for maintain same content offset.
if hideSearchBar == true {
self.tableView.contentOffset = CGPointMake(0, self.tableView.tableHeaderView!.bounds.height - self.tableView.contentInset.top)
}
Finally implement below methods.
override func scrollViewDidScroll(scrollView: UIScrollView) {
if self.tableView.tableHeaderView!.bounds.height - self.tableView.contentInset.top == self.tableView.contentOffset.y && self.tableView.dragging == false {
hideSearchBar = true
}
else if self.tableView.dragging == true {//Reset hiding process after user dragging
hideSearchBar = false
}
}
func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if self.tableView.contentOffset.y + self.tableView.contentInset.top <= self.tableView.tableHeaderView!.bounds.height
{
self.tableView.contentOffset = CGPointMake(0, self.tableView.tableHeaderView!.bounds.height - self.tableView.contentInset.top)
}
}
Try to set for TVC
self.automaticallyAdjustsScrollViewInsets = NO
This is a problem caused by hidesBottomBarWhenPushed = YES , if you uncheck Hide Bottom Bar On Push, the searchBar will not appear when VC back to TVC.
Try this in TableViewController.m:
- (void)viewDidLayoutSubviews{
[super viewDidLayoutSubviews];
[self hideSearchBar];
}
I can't explain why but I know that if hidesBottomBarWhenPushed = YES for UITabBarController to push vc, viewDidLayoutSubviews will be called more than once when the view appears again. First time subviews keep the same position, whereas the second time be called, subviews will be adjusted for some reason to relayout with the most original position, which is very weird. Do your custom layout in viewDidLayoutSubviews will prevent this to happen even after viewDidAppear.
My solution is a little stupid.
Add this method to the sample code.
- (void)viewWillLayoutSubviews
{
[self hideSearchBar];
}
It seems the tableView will redraw the scrollView inside it.
Since the tableView reset the contentOffset, I made a custom tableView has property to save the hidden status of search bar.Below is the code.Hope it helps.
//
// TableViewController.m
// SearchBarJump
//
// Created by Eyal Cohen on 3/9/14.
// Copyright (c) 2014 Eyal. All rights reserved.
//
#import "TableViewController.h"
#interface CustomTableView : UITableView
#property (nonatomic, assign, getter = isSearchBarHidden)BOOL searchBarHidden;
#end
#implementation CustomTableView
#synthesize searchBarHidden = _searchBarHidden;
- (void)layoutSubviews
{
[super layoutSubviews];
if (self.isSearchBarHidden) {
[self hideSearchBar:NO];
}
}
- (void)setSearchBarHidden:(BOOL)searchBarHidden
{
_searchBarHidden = searchBarHidden;
if (_searchBarHidden && self.contentOffset.y != self.tableHeaderView.frame.size.height) {
[self hideSearchBar:YES];
}
}
- (void)hideSearchBar:(BOOL)animated {
// hide search bar
[self setContentOffset:CGPointMake(0.0, self.tableHeaderView.frame.size.height) animated:animated];
}
#end
#interface TableViewController () {
NSInteger _rows;
}
#property (nonatomic, weak) IBOutlet CustomTableView *mainTable;
#end
#implementation TableViewController
#synthesize mainTable = _mainTable;
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.view = _mainTable;
[_mainTable setDelegate:self];
[_mainTable setDataSource:self];
_rows = 3;
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.mainTable setSearchBarHidden:YES];
}
- (void)hideSearchBar {
// hide search bar
[_mainTable setContentOffset:CGPointMake(0.0, self.tableView.tableHeaderView.frame.size.height) animated:NO];
}
- (IBAction)toggleCount:(UIBarButtonItem *)sender {
if (_rows == 20) {
_rows = 3;
} else {
_rows = 20;
}
[_mainTable reloadData];
}
- (IBAction)hideBar:(UIBarButtonItem *)sender {
[self hideSearchBar];
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
return _rows;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
// Configure the cell...
cell.textLabel.text = #"cell";
return cell;
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
[_mainTable setSearchBarHidden:NO];
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
if (_mainTable.contentOffset.y == _mainTable.tableHeaderView.bounds.size.height) {
[_mainTable setSearchBarHidden:YES];
}
}
#end
UITableViewController always modifies its UITableviews content offset in its viewDidAppear to make sure that its all rows are visible. So your hacky methods don't work here.
There are several solution to this problem. The one I selected is shown below
First delete that searchBar from your storyboard.
#interface TableViewController () {
NSInteger _rows;
}
#end
#implementation TableViewController
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
_rows = 4; // +1 for searchBar
}
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
}
- (void)hideSearchBar {
// hide search bar
[[self tableView] scrollToRowAtIndexPath:[NSIndexPath indexPathWithIndex:1] atScrollPosition:UITableViewScrollPositionTop animated:NO];
}
- (IBAction)toggleCount:(UIBarButtonItem *)sender {
if (_rows == 20) {
_rows = 4;
} else {
_rows = 20;
}
[self.tableView reloadData];
}
- (IBAction)hideBar:(UIBarButtonItem *)sender {
[self hideSearchBar];
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
return _rows;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if(indexPath.row == 0){
UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
UISearchBar *searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 0, tableView.frame.size.width,44)];
[cell addSubview:searchBar];
return cell;
}
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
// Configure the cell...
cell.textLabel.text = #"cell";
return cell;
}
#end
The above solution just ensures that automatic scrolling magic is disabled.
If you want your default searchBar to be hidden override UITableView and call hideSearchBar when tableview is initially loaded for the first time.
I fix the bug just like this:
#interface NTTableView : UITableView
#end
#implementation NTTableView
-(void)setContentOffset:(CGPoint)contentOffset{
if (self.contentOffset.y==-20&&
contentOffset.y==-64) {
NSLog(#"iOS7 bug here, FML");
}else{
[super setContentOffset:contentOffset];
}
}
#end
Fix for my somewhat similar situation with a UISearchBar as the tableHeaderView. Not sure if this falls into the same exact scenario, but it hides the search bar when the view appears. (Being unconcerned with the amount of rows in the table view)
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:NO];
}
Setting edgesForExtendedLayout to [.top, .bottom] instead of just .top on TVC fixed problem for me.
Of course, automaticallyAdjustsScrollViewInsets is set to false
EDIT: seems that it only works if tvc.tabBar is translucent
As a weird hack I can only suggest to add an empty cell to the end of cells with height about 400
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
return _rows + 1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
if(indexPath.row == _rows)
{
//cellEmpty - cell identifier in storyboard
cell = [tableView dequeueReusableCellWithIdentifier:#"cellEmpty" forIndexPath:indexPath];
}
else
{
cell.textLabel.text = #"cell";
}
// Configure the cell...
return cell;
}
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if(indexPath.row == _rows)
{
return 400;
}
else
{
return 44;
}
}
your output file
https://github.com/iDevAndroid/SearchBarJump
simply use this code don't make do complex for that
-(void)viewDidDisappear:(BOOL)animated{
[self.tableView setContentInset:UIEdgeInsetsMake(-0.3, 0, 0, 0)];
[super viewDidAppear:animated];
}
here is one problem if you are set UIEdgeInsetsMake(0, 0, 0, 0) the searchBar jumping as in original mode
I am stucked in a stupid problem since two days. I have got a UITableViewController pushed in Navigation Controller. When it loads, since there is no data, so empty table is visible:
But when I receive data from server, and call [self.tableView reloadData], both numberOfRowsInSection and heightForRowAtIndexPath get invoke except cellForRowAtIndexPath and my controller is shown without table:
I can't really understand that why it is happening. All datasource methods are called except for cellForRowAtIndexPath. Please someone guide me... Thanks..
ActLogController.h
#interface ActLogController : UITableViewController<ASIHTTPRequestDelegate,UITableViewDataSource,UITableViewDelegate>
#property(strong) NSMutableArray *activityKeys;
#end
ActLogController.m
- (void)viewDidLoad
{
[super viewDidLoad];
activityKeys = [[NSMutableArray alloc] init];
self.tableView.dataSource = self;
}
-(void) viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self retrieveActivityLogFromServer];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return activityKeys.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if(cell == nil)
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
// Configure the cell...
ActivityLogUnit *act = [activityKeys objectAtIndex:indexPath.row];
cell.textLabel.text = act.price;
return cell;
}
-(CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return 50.0;
}
-(void) requestFinished:(ASIHTTPRequest *)request
{
NSArray *list = [request.responseString JSONValue];
for (int i = 0; i < list.count; i++) {
NSArray *singleTrade = [[list objectAtIndex:i] JSONValue];
ActivityLogUnit *unit = [[ActivityLogUnit alloc] init];
unit.symbol = [singleTrade objectAtIndex:0];
unit.type = [singleTrade objectAtIndex:1];
unit.price = [singleTrade objectAtIndex:2];
unit.volTraded = [singleTrade objectAtIndex:3];
unit.remVol = [singleTrade objectAtIndex:4];
unit.actualVol = [singleTrade objectAtIndex:5];
unit.recordID = [singleTrade objectAtIndex:6];
unit.orderNo = [singleTrade objectAtIndex:7];
unit.time = [singleTrade objectAtIndex:8];
[activityKeys addObject:unit];
}
if(activityKeys.count > 0)
{
[self.tableView reloadData];//it is called and I have got 6 items confirm
}
}
EDIT
I set some dummy data in my array activityKeys, Data is being displayed in table, and cellforrowatindexpath is called successfully. But as I reload data after sometime, other methods are called except this one and table disappears as shown in 2nd pic. Any ideas?
Your problem is that you probably download the data content on a background thread. Since you cannot update the UI on a background you need to call [self.tableView reloadData] on the main thread once the download is finished!
Hope it helps!
Looks like you in secondary thread, do reloadData in main thread by using following code
[self.tableView performSelectorOnMainThread#selector(reloadData) withObject:nil waitUntilDone:NO]
You can always use [NSThread isMainThread] to check whether you are in main thread or not.
you have to write in viewdidload
self.tableView.dataSource = self;
self.tableView.delegate = self;
Edit
You have no xib then where you are declared/sets your tableview's properties. Like
self.tableView.frame = CGRectMake(0, 45, 320, 500);
self.tableView.rowHeight = 34.0f;
self.tableView.separatorStyle=UITableViewCellSeparatorStyleNone;
[self.view addSubview:self.tableView];
[self.tableView setBackgroundColor:[UIColor clearColor]];
self.tableView.showsVerticalScrollIndicator=NO;
self.tableView.dataSource = self;
self.tableView.delegate = self;
Try with
#property(nonatomic,strong) NSMutableArray *activityKeys;
Firstly I strongly believe that the instance name of the tableview should not be similar to the local variable (i.e. tableView in class should not be equal to tableView in delegate and data source methods).
Second in your question posted I could not see the delegate set for the table view.
answer Posted By Samir Rathod should work if you have #property for the table view set in you .h or .m file.
You can also do this if you have a XIB file.
Press ctrl and click + drag the tableview to the files owner and set the delegate and datasource.
For me the problem was my stubbed-out code returning 0 as the number of sections (so it never asked how many rows were in the section, and never got their data). Just change that to 1 if it's your problem also. Additionally, I was working in Swift, where the issue mentioned by #shahid-rasheed is coded (slightly) differently:
dispatch_async(dispatch_get_main_queue(), {
self.tableView.reloadData()
})
At last I got it worked. cellForRowAtIndexPath was not being called because of a line of code I didn't mention here... which was actually removing some color background layer from view. It was causing reloading issue. After removing it, everything works fine.
Thank you all of you for your cooperation :)
I had the same symptoms too. In my case, the first time I loaded the data (from core data) in viewDidLoad, NSSortDescriptor was used to sort the data.
On the click of a button, the core data was fetched again (this time with changes) and tableView data reloaded. It initially gave me a blank table view after the button was clicked because I forgot to sort the data the second time I fetched it.
Learning points: Remember to call all methods which modify the cell (like background color mentioned by iAnum, or NSSortDescriptor in my case) if you have used them in the beginning!
Is there some way to find out when a UITableView has finished asking for data from its data source?
None of the viewDidLoad/viewWillAppear/viewDidAppear methods of the associated view controller (UITableViewController) are of use here, as they all fire too early. None of them (entirely understandably) guarantee that queries to the data source have finished for the time being (eg, until the view is scrolled).
One workaround I have found is to call reloadData in viewDidAppear, since, when reloadData returns, the table view is guaranteed to have finished querying the data source as much as it needs to for the time being.
However, this seems rather nasty, as I assume it is causing the data source to be asked for the same information twice (once automatically, and once because of the reloadData call) when it is first loaded.
The reason I want to do this at all is that I want to preserve the scroll position of the UITableView - but right down to the pixel level, not just to the nearest row.
When restoring the scroll position (using scrollRectToVisible:animated:), I need the table view to already have sufficient data in it, or else the scrollRectToVisible:animated: method call does nothing (which is what happens if you place the call on its own in any of viewDidLoad, viewWillAppear or viewDidAppear).
This answer doesn't seem to be working anymore, due to some changes made to UITableView implementation since the answer was written. See this comment : Get notified when UITableView has finished asking for data?
I've been playing with this problem for a couple of days and think that subclassing UITableView's reloadData is the best approach :
- (void)reloadData {
NSLog(#"BEGIN reloadData");
[super reloadData];
NSLog(#"END reloadData");
}
reloadData doesn't end before the table has finish reload its data. So, when the second NSLog is fired, the table view has actually finish asking for data.
I've subclassed UITableView to send methods to the delegate before and after reloadData. It works like a charm.
I did have a same scenario in my app and thought would post my answer to you guys as other answers mentioned here does not work for me for iOS7 and later
Finally this is the only thing that worked out for me.
[yourTableview reloadData];
dispatch_async(dispatch_get_main_queue(),^{
NSIndexPath *path = [NSIndexPath indexPathForRow:yourRow inSection:yourSection];
//Basically maintain your logic to get the indexpath
[yourTableview scrollToRowAtIndexPath:path atScrollPosition:UITableViewScrollPositionTop animated:YES];
});
Swift Update:
yourTableview.reloadData()
dispatch_async(dispatch_get_main_queue(), { () -> Void in
let path : NSIndexPath = NSIndexPath(forRow: myRowValue, inSection: mySectionValue)
//Basically maintain your logic to get the indexpath
yourTableview.scrollToRowAtIndexPath(path, atScrollPosition: UITableViewScrollPosition.Top, animated: true)
})
So how this works.
Basically when you do a reload the main thread becomes busy so at that time when we do a dispatch async thread, the block will wait till the main thread gets finished. So once the tableview has been loaded completely the main thread will gets finish and so it will dispatch our method block
Tested in iOS7 and iOS8 and it works awesome;)
Update for iOS9: This just works fine is iOS9 also. I have created a sample project in github as a POC.
https://github.com/ipraba/TableReloadingNotifier
I am attaching the screenshot of my test here.
Tested Environment: iOS9 iPhone6 simulator from Xcode7
EDIT: This answer is actually not a solution. It probably appears to work at first because reloading can happen pretty fast, but in fact the completion block doesn't necessarily get called after the data has fully finished reloading - because reloadData doesn't block. You should probably search for a better solution.
To expand on #Eric MORAND's answer, lets put a completion block in. Who doesn't love a block?
#interface DUTableView : UITableView
- (void) reloadDataWithCompletion:( void (^) (void) )completionBlock;
#end
and...
#import "DUTableView.h"
#implementation DUTableView
- (void) reloadDataWithCompletion:( void (^) (void) )completionBlock {
[super reloadData];
if(completionBlock) {
completionBlock();
}
}
#end
Usage:
[self.tableView reloadDataWithCompletion:^{
//do your stuff here
}];
reloadData just asking for data for the visible cells. Says, to be notified when specify portion of your table is loaded, please hook the tableView: willDisplayCell: method.
- (void) reloadDisplayData
{
isLoading = YES;
NSLog(#"Reload display with last index %d", lastIndex);
[_tableView reloadData];
if(lastIndex <= 0){
isLoading = YES;
//Notify completed
}
- (void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
if(indexPath.row >= lastIndex){
isLoading = NO;
//Notify completed
}
That is my solution. 100% works and used in many projects. It's a simple UITableView subclass.
#protocol MyTableViewDelegate<NSObject, UITableViewDelegate>
#optional
- (void)tableViewWillReloadData:(UITableView *)tableView;
- (void)tableViewDidReloadData:(UITableView *)tableView;
#end
#interface MyTableView : UITableView {
struct {
unsigned int delegateWillReloadData:1;
unsigned int delegateDidReloadData:1;
unsigned int reloading:1;
} _flags;
}
#end
#implementation MyTableView
- (id<MyTableViewDelegate>)delegate {
return (id<MyTableViewDelegate>)[super delegate];
}
- (void)setDelegate:(id<MyTableViewDelegate>)delegate {
[super setDelegate:delegate];
_flags.delegateWillReloadData = [delegate respondsToSelector:#selector(tableViewWillReloadData:)];
_flags.delegateDidReloadData = [delegate respondsToSelector:#selector(tableViewDidReloadData:)];
}
- (void)reloadData {
[super reloadData];
if (_flags.reloading == NO) {
_flags.reloading = YES;
if (_flags.delegateWillReloadData) {
[(id<MyTableViewDelegate>)self.delegate tableViewWillReloadData:self];
}
[self performSelector:#selector(finishReload) withObject:nil afterDelay:0.0f];
}
}
- (void)finishReload {
_flags.reloading = NO;
if (_flags.delegateDidReloadData) {
[(id<MyTableViewDelegate>)self.delegate tableViewDidReloadData:self];
}
}
#end
It's similar to Josh Brown's solution with one exception. No delay is needed in performSelector method. No matter how long reloadData takes. tableViewDidLoadData: always fires when tableView finishes asking dataSource cellForRowAtIndexPath.
Even if you do not want to subclass UITableView you can simply call [performSelector:#selector(finishReload) withObject:nil afterDelay:0.0f] and your selector will be called right after the table finishes reloading. But you should ensure that selector is called only once per call to reloadData:
[self.tableView reloadData];
[self performSelector:#selector(finishReload) withObject:nil afterDelay:0.0f];
Enjoy. :)
This is an answer to a slightly different question: I needed to know when UITableView had also finished calling cellForRowAtIndexPath(). I subclassed layoutSubviews() (thanks #Eric MORAND) and added a delegate callback:
SDTableView.h:
#protocol SDTableViewDelegate <NSObject, UITableViewDelegate>
#required
- (void)willReloadData;
- (void)didReloadData;
- (void)willLayoutSubviews;
- (void)didLayoutSubviews;
#end
#interface SDTableView : UITableView
#property(nonatomic,assign) id <SDTableViewDelegate> delegate;
#end;
SDTableView.m:
#import "SDTableView.h"
#implementation SDTableView
#dynamic delegate;
- (void) reloadData {
[self.delegate willReloadData];
[super reloadData];
[self.delegate didReloadData];
}
- (void) layoutSubviews {
[self.delegate willLayoutSubviews];
[super layoutSubviews];
[self.delegate didLayoutSubviews];
}
#end
Usage:
MyTableViewController.h:
#import "SDTableView.h"
#interface MyTableViewController : UITableViewController <SDTableViewDelegate>
#property (nonatomic) BOOL reloadInProgress;
MyTableViewController.m:
#import "MyTableViewController.h"
#implementation MyTableViewController
#synthesize reloadInProgress;
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
if ( ! reloadInProgress) {
NSLog(#"---- numberOfSectionsInTableView(): reloadInProgress=TRUE");
reloadInProgress = TRUE;
}
return 1;
}
- (void)willReloadData {}
- (void)didReloadData {}
- (void)willLayoutSubviews {}
- (void)didLayoutSubviews {
if (reloadInProgress) {
NSLog(#"---- layoutSubviewsEnd(): reloadInProgress=FALSE");
reloadInProgress = FALSE;
}
}
NOTES:
Since this is a subclass of UITableView which already has a delegate property pointing to MyTableViewController there's no need to add another one. The "#dynamic delegate" tells the compiler to use this property. (Here's a link describing this: http://farhadnoorzay.com/2012/01/20/objective-c-how-to-add-delegate-methods-in-a-subclass/)
The UITableView property in MyTableViewController must be changed to use the new SDTableView class. This is done in the Interface Builder Identity Inspector. Select the UITableView inside of the UITableViewController and set its "Custom Class" to SDTableView.
I had found something similar to get notification for change in contentSize of TableView. I think that should work here as well since contentSize also changes with loading data.
Try this:
In viewDidLoad write,
[self.tableView addObserver:self forKeyPath:#"contentSize" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionPrior context:NULL];
and add this method to your viewController:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:#"contentSize"]) {
DLog(#"change = %#", change.description)
NSValue *new = [change valueForKey:#"new"];
NSValue *old = [change valueForKey:#"old"];
if (new && old) {
if (![old isEqualToValue:new]) {
// do your stuff
}
}
}
}
You might need slight modifications in the check for change. This had worked for me though.
Cheers! :)
Here's a possible solution, though it's a hack:
[self.tableView reloadData];
[self performSelector:#selector(scrollTableView) withObject:nil afterDelay:0.3];
Where your -scrollTableView method scrolls the table view with -scrollRectToVisible:animated:. And, of course, you could configure the delay in the code above from 0.3 to whatever seems to work for you. Yeah, it's ridiculously hacky, but it works for me on my iPhone 5 and 4S...
I had something similar I believe. I added a BOOL as instance variable which tells me if the offset has been restored and check that in -viewWillAppear:. When it has not been restored, I restore it in that method and set the BOOL to indicate that I did recover the offset.
It's kind of a hack and it probably can be done better, but this works for me at the moment.
It sounds like you want to update cell content, but without the sudden jumps that can accompany cell insertions and deletions.
There are several articles on doing that. This is one.
I suggest using setContentOffset:animated: instead of scrollRectToVisible:animated: for pixel-perfect settings of a scroll view.
You can try the following logic:
-(UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"MyIdentifier"];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:#"MyIdentifier"];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
if ( [self chkIfLastCellIndexToCreate:tableView :indexPath]){
NSLog(#"Created Last Cell. IndexPath = %#", indexPath);
//[self.activityIndicator hide];
//Do the task for TableView Loading Finished
}
prevIndexPath = indexPath;
return cell;
}
-(BOOL) chkIfLastCellIndexToCreate:(UITableView*)tableView : (NSIndexPath *)indexPath{
BOOL bRetVal = NO;
NSArray *visibleIndices = [tableView indexPathsForVisibleRows];
if (!visibleIndices || ![visibleIndices count])
bRetVal = YES;
NSIndexPath *firstVisibleIP = [visibleIndices objectAtIndex:0];
NSIndexPath *lastVisibleIP = [visibleIndices objectAtIndex:[visibleIndices count]-1];
if ((indexPath.row > prevIndexPath.row) && (indexPath.section >= prevIndexPath.section)){
//Ascending - scrolling up
if ([indexPath isEqual:lastVisibleIP]) {
bRetVal = YES;
//NSLog(#"Last Loading Cell :: %#", indexPath);
}
} else if ((indexPath.row < prevIndexPath.row) && (indexPath.section <= prevIndexPath.section)) {
//Descending - scrolling down
if ([indexPath isEqual:firstVisibleIP]) {
bRetVal = YES;
//NSLog(#"Last Loading Cell :: %#", indexPath);
}
}
return bRetVal;
}
And before you call reloadData, set prevIndexPath to nil. Like:
prevIndexPath = nil;
[mainTableView reloadData];
I tested with NSLogs, and this logic seems ok. You may customise/improve as needed.
finally i have made my code work with this -
[tableView scrollToRowAtIndexPath:scrollToIndex atScrollPosition:UITableViewScrollPositionTop animated:YES];
there were few things which needed to be taken care of -
call it within "- (UITableViewCell *)MyTableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath"
just ensure that "scrollToRowAtIndexPath" message is sent to relevant instance of UITableView, which is definitely MyTableview in this case.
In my case UIView is the view which contains instance of UITableView
Also, this will be called for every cell load. Therefore, put up a logic inside "cellForRowAtIndexPath" to avoid calling "scrollToRowAtIndexPath" more than once.
You can resize your tableview or set it content size in this method when all data loaded:
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
tableView.frame =CGRectMake(tableView.frame.origin.x, tableView.frame.origin.y, tableView.frame.size.width, tableView.contentSize.height);
}
I just run repeating scheduled timer and invalidate it only when table's contentSize is bigger when tableHeaderView height (means there is rows content in the table). The code in C# (monotouch), but I hope the idea is clear:
public override void ReloadTableData()
{
base.ReloadTableData();
// don't do anything if there is no data
if (ItemsSource != null && ItemsSource.Length > 0)
{
_timer = NSTimer.CreateRepeatingScheduledTimer(TimeSpan.MinValue,
new NSAction(() =>
{
// make sure that table has header view and content size is big enought
if (TableView.TableHeaderView != null &&
TableView.ContentSize.Height >
TableView.TableHeaderView.Frame.Height)
{
TableView.SetContentOffset(
new PointF(0, TableView.TableHeaderView.Frame.Height), false);
_timer.Invalidate();
_timer = null;
}
}));
}
}
Isn't UITableView layoutSubviews called just before the table view displays it content? I've noticed that it is called once the table view has finished load its data, maybe you should investigate in that direction.
Since iOS 6 onwards, the UITableview delegate method called:
-(void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section
will execute once your table reloads successfully. You can do customisation as required in this method.
The best solution I've found in Swift
extension UITableView {
func reloadData(completion: ()->()) {
self.reloadData()
dispatch_async(dispatch_get_main_queue()) {
completion()
}
}
}
Why no just extend?
#interface UITableView(reloadComplete)
- (void) reloadDataWithCompletion:( void (^) (void) )completionBlock;
#end
#implementation UITableView(reloadComplete)
- (void) reloadDataWithCompletion:( void (^) (void) )completionBlock {
[self reloadData];
if(completionBlock) {
completionBlock();
}
}
#end
scroll to the end:
[self.table reloadDataWithCompletion:^{
NSInteger numberOfRows = [self.table numberOfRowsInSection:0];
if (numberOfRows > 0)
{
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:numberOfRows-1 inSection:0];
[self.table scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:NO];
}
}];
Not tested with a lot of data