I have a ViewController in which I put another small UIView in the middle where I have placed a TableView but not sure how to display data from an array in this table, any help would be greatly appreciated. I'm trying to load the table using this code:
-(void) animateResults {
_resultsLabel.text = [NSString stringWithFormat:#"You Scored %d", runningScore ];
resultsTable.delegate = self;
resultsTable.dataSource = self;
[self.resultsTable registerClass:[resultsViewCell self] forCellReuseIdentifier:#"resultsListCell"];
[self.resultsTable reloadData];
[self.view layoutIfNeeded];
[UIView animateWithDuration:0.3 animations:^{
_resultsView.frame = self.view.frame;
} completion:^(BOOL finished){
NSLog(#"%#", questionsArray);
}];
}
I am using a Custom Cell in my TableView to load an NSMutableArray. I have tried Using this code for the table view:
-(NSInteger)numberOfSectionsInTableView:(resultsViewCell *)tableView {
//Return number of sections
return 1;
}
//get number of rows by counting number of challenges
-(NSInteger)tableView:(resultsViewCell *)tableView numberOfRowsInSection:(NSInteger)section {
return questionsArray.count;
}
//setup cells in tableView
-(resultsViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
//setup cell
static NSString *CellIdentifier = #"resultsListCell";
resultsViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
NSDictionary *results = [questionsArray objectAtIndex:indexPath.row];
NSString *resultsName = [results objectForKey:#"answers"];
BOOL correct = [[results objectForKey:#"correct"] boolValue];
if (!correct) {
cell.resultsIcon.image = [UIImage imageNamed:#"BlackIconLock.png"];
}
else{
cell.resultsIcon.image = nil;
}
cell.resultsName.text = resultsName;
return cell;
}
But the _resultsView does not load not sure why. I've got a lot of great help thus far and I really do appreciate it. I've been stuck at this point for about 2 weeks. Please help!
If you are using above xcode 7, you will use UIContainerView middle to ViewController in storyboard, its automatically Embed the UITableView to containerView, example
Try to reload data in animateResults in completion block. something like this,
completion:^(BOOL finished){
NSLog(#"%#", questionsArray);
[self.resultsTable reloadData];
}];
I FINALLY! got this figured out My intention was to have a UITableView load inside of a UIView. I tried to drag and drop the TableView delegate and datasource on the ViewController but every time the app loaded it crashed. So I moved the table view delegate and datasource to the UIView load method here: I tried using this Method to do so.
-(void) animateResults {
//this part of my method actually worked it displayed the final score from a predifined variable.
_resultsLabel.text = [NSString stringWithFormat:#"You Scored %d", runningScore ];
//i tried using this to set my UITableView delegate & dataSource when the method was initiated instead of UIViewController load.
resultsTable.delegate = self;
resultsTable.dataSource = self;
//used this code to register UIViewCell that was nonexistent,
//i declared *CellIdentifier = #"resultsListCell" and it should have
//been *CellIdentifier = #"resultsCell". so i received an error used this code to solve it.
//duh. cell don't exist must not be the cell you created.My Bad.
[self.resultsTable registerClass:[resultsViewCell self] forCellReuseIdentifier:#"resultsListCell"];
//this code worked but after debugging i found out there was no data to load.
[self.resultsTable reloadData];
//was desperate at this point not sure why i put this in.
[self.view layoutIfNeeded];
//this also worked loads my UIView.
[UIView animateWithDuration:0.3 animations:^{
_resultsView.frame = self.view.frame;
} completion:^(BOOL finished){
NSLog(#"%#", questionsArray);
}];
}
So My solution was frustrating in it's simplicity. The reason my app crashed when I tried to drag and drop the dataSource and delegate, was because of the mislabeled "static NSString *CellIdentifier", So make sure your cell identifier is correct! Not knowing this I attempted to load the UITableView dataSource and delegate in the above method. I have since modified the above method to this.
-(void) animateResults {
//populate label
_resultsLabel.text = [NSString stringWithFormat:#"You Scored %d", runningScore ];
//reload table
[self.resultsTable reloadData];
//Load View
[UIView animateWithDuration:0.3 animations:^{
_resultsView.frame = self.view.frame;
} completion:^(BOOL finished){
NSLog(#"%#", questionsArray);
}];
}
This trimmed down code works because I connected the dataSource and delegate to UIViewController. Doing this loads the table when the UIViewController loads so theres no need to do so programmatically. Also, correcting the mislabeled cell identifier eliminated the need for the registerClass variable. Hope this Helps.
Related
I have added some animation to a table view to make it look nicer when it loads. In the controller's viewDidLoad I make an asynchronous request for data and when it returns the table view is populated.
When my table loads the cells are revealed one by one. (I took inspiration from this excellent guide).
- (void)tableFadeInAnimation {
//[_venueTableView reloadData];
NSArray<UITableViewCell *> *cells = _venueTableView.visibleCells;
NSInteger index = 0;
for (UITableViewCell * m in cells){
UITableViewCell *cell = m;
cell.alpha = 0;
[UIView animateWithDuration:1 delay:0.25 * index options:0 animations:^(){
cell.alpha = 1;
} completion:nil];
NSLog(#"end of table animation");
index += 1;
}
}
My problem with running this as an initialising function is that once this finishes my table has no more animations to perform. I then took this principle to cellForRowAtIndexPath (removing the loop).
cell.alpha = 0;
[UIView animateWithDuration:2.0 animations:^(){
cell.alpha = 1;
}];
This would load all the cells together but would animate new cells appearing on the table.
cell.alpha = 0;
[UIView animateWithDuration:1 delay:0.05 * indexPath.row options:0 animations:^(){
cell.alpha = 1;
} completion:^(BOOL finished){
NSLog(#"animation complete");
}];
This made the table load each cell 1 by 1 however it is tied to all the cells (not the visible ones) so the further you go down the table, the longer the loading time for the cell.
Also when you move back up the table, all the older cells reanimate onto the table. I want the old cells to remain and the new cells to animate. Is there a way I can keep track of which cells have been loaded and only animate brand new, never before seen cells?
You should have a property to keep track index of last cell which is displayed (name lastCellDisplayedIndex). Only animate cells which have index less than lastCellDisplayedIndex. Each time call reloadData, reset lastCellDisplayedIndex = -1.
Try my below code.
#property (nonatomic, assign) NSUInteger lastCellDisplayed;
- (void)viewDidLoad {
[super viewDidLoad];
[self reloadTableView];
}
// Use this method each time you want to reload data of tableView
// instead of |reloadData| method
- (void)reloadTableView {
_lastCellDisplayedIndex = -1;
[self.tableView reloadData];
}
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
// Update |_lastCellDisplayedIndex| each time a cell is displayed
_lastCellDisplayedIndex = MAX(indexPath.row, _lastCellDisplayedIndex);
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
...
// Only animate cells which have |indexPath.row| < |_lastCellDisplayedIndex|
if (_lastCellDisplayedIndex < indexPath.row) {
cell.alpha = 0;
[UIView animateWithDuration:1 delay:0.05 * indexPath.row options:0 animations:^(){
cell.alpha = 1;
} completion:^(BOOL finished){
NSLog(#"animation complete");
}];
} else {
cell.alpha = 1;
}
...
}
The best approach for this is to add your animation block and any change to your cell's frame or alpha, in the tableView:willDisplayCell:forRowAtIndexPath: method of your UITableViewDelegate
My recommended approach, assuming you have some backing data source to provide data in cellForRowAtIndexPath, is to add some mutable property hasBeenDisplayed to your model objects (or an NSDictionary that maps each model object to a bool that indicates whether or not it has been displayed). This logic is complicated enough that you want some logic code to ensure the consistency of the view code. Then, once you have this property, you can call your custom animation in cellForRowAtIndexPath if the cell has not yet been displayed.
Thanks to #trungduc for the answer, I'm posting the completed solution to this in the hopes people will find it useful. To stop the table drawing cells that have already appeared you need to implement a variable to track the maximum index that has been displayed on the table, lastCellDisplayedIndex. In #trungduc's answer he put this variable in the - (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath method however I found this created some errors making some cells redraw themselves. I had a read up on the difference between the cellForRow and cellWillDisplay methods and it seemed like the best place to put the animation was cellWillDisplay as the cell has been initialised and is apparently the place you should be performing UI tweaks to a cell (like animations!).
This method looks like this:
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{
lastCellDisplayedIndex = MAX(indexPath.row, lastCellDisplayedIndex);
NSLog(#"lastCellDisplayedIndex = %ld, indexPath for Cell = %ld", lastCellDisplayedIndex, indexPath.row);
if (lastCellDisplayedIndex <= indexPath.row){
cell.alpha = 0;
[UIView animateWithDuration:2.0 animations:^(){
cell.alpha = 1;
}];
if (lastCellDisplayedIndex == totalCellsToDisplay - 1){
NSLog(#"END OF TABLE ANIMATIONS!");
lastCellDisplayedIndex = totalCellsToDisplay + 1;
}
}
else {
cell.alpha = 1;
}
}
This method handles almost everything. It will first change the value of lastCellDisplayedIndex to the value of the max index the table has seen. Next it will decide whether the cell it is handling should be animated or left as is. I also had to add a guard variable (of sorts), totalCellsToDisplay would act as your tables datasource array: -
(NSInteger)tableView:(nonnull UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return totalCellsToDisplay;
}
So in your real app you would instead have
- (NSInteger)tableView:(nonnull UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return yourTableDataArray.count;
}
The reason I am checking the maximum number of cells being drawn is if you just have this code:
lastCellDisplayedIndex = MAX(indexPath.row, lastCellDisplayedIndex);
if (lastCellDisplayedIndex <= indexPath.row){}
then the maximum index will never go higher than the final cell, so this cell will be reanimated every-time you scroll up and down. To fix this when the indexPath = the total cells - 1 (because of zero index) then you bump the value of lastCellDisplayedIndex up so that no more cells will ever get drawn.
Finally we need to solve the issue of how many cells the table will initially draw. I'm not sure quite how this works but in my testing it would always draw 15 cells (if I returned more than 15). Anyway I implemented both my staggered load animation and fixed this problem with my loading animation function.
- (void)tableFadeInAnimation {
[_myTable reloadData];
NSArray<UITableViewCell *> *cells = _myTable.visibleCells;
NSInteger index = 0;
for (UITableViewCell * m in cells){
UITableViewCell *cell = m;
cell.alpha = 0;
[UIView animateWithDuration:0.5 delay:0.25 * index options:0 animations:^(){
cell.alpha = 1;
} completion:^(BOOL finished){
lastCellDisplayedIndex = _myTable.visibleCells.count;
NSLog(#"Table Animation Finished, lastCellDisplayed Index = %ld", lastCellDisplayedIndex);
}];
NSLog(#"end of table animation");
index += 1;
}
}
I used the completion block of the function to set the value of lastCellDisplayed equal to the number of cells that are visible. Now the table view will animate all new cells.
Hope this helps and thanks to #trungduc for the answer!
I have a class that allows a user to add entries to a server-side table. Everything works correctly until I attempt to refresh the UITableView with the new data. I make a server call to get the new dataset, use it to refresh the NSArray that is the data source for the table, and then attempt to reload the table. Here is the method that is called when the data comes back from the server:
- (void) logEntriesRefreshed : (NSNotification *) notification {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:#"log_entries_refreshed"
object:nil];
NSLog(#"returned from log entries fetch");
_logEntriesArray = [LogEntriesDataFetcher getLogEntriesArray];
[_tableView reloadData];
_activityIndicator.hidden = YES;
[_activityIndicator stopAnimating];
NSLog(#"log entries array count: %lu", [_logEntriesArray count]);
[_tableView selectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]
animated:NO
scrollPosition:UITableViewScrollPositionNone];
}
It's this last line that is the problem. I want to programmatically select the first row in the table (there has to be at least one, since I just added a row). But it appears that this line never executes. Note this method, which should go next:
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSLog(#"here");
UITableViewCell *previousCell = (UITableViewCell *)[_tableView cellForRowAtIndexPath:_previousIndexPath];
previousCell.backgroundColor = [UIColor clearColor];
previousCell.textLabel.textColor = [SharedVisualElements primaryFontColor];
UITableViewCell *cell = (UITableViewCell *)[_tableView cellForRowAtIndexPath:indexPath];
cell.contentView.backgroundColor = [SharedVisualElements secondaryFontColor];
cell.textLabel.textColor = [SharedVisualElements primaryFontColor];
_previousIndexPath = indexPath;
// get the file attributes for the cell just selected
_currentEntry = (LogEntry *)[_logEntriesArray objectAtIndex:[indexPath row]];
NSLog(#"array count: %lu", (unsigned long)[_logEntriesArray count]);
NSLog(#"current entry: %ld", (long)[indexPath row]);
_isExistingEntry = YES;
_arrayPositionOfEntryBeingEdited = [indexPath row];
[self initializeValues];
[self initializeObjects];
[self captureStartingValuesForStateMachine];
}
I have break points set on the selectRowAtIndexPath line and also on the first NSLog(#"here") line in didSelectRow.... I get to the selectRowAtIndexPath line but never to the didSelectRow method. My console output is consistent with that:
returned from log entries fetch
log entries array count: 7
and that is the end of it. Nothing from the didSelectRow... method. There are no errors thrown, either.
What am I missing. Seems pretty straightforward, but nothing I do seems to work.
As per Apple's documentation, calling selectRowAtIndexPath will NOT invoke the didSelectRowAtIndexPath. Take a look here.
Calling this method does not cause the delegate to receive a
tableView:willSelectRowAtIndexPath: or
tableView:didSelectRowAtIndexPath: message, nor does it send
UITableViewSelectionDidChangeNotification notifications to observers.
To specifically invoke the didSelectRowAtIndexPath delegate method, use the following code:
[[tableView delegate] tableView:tableView didSelectRowAtIndexPath:indexPath];
Hope this helps.
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!
So, simply put, I have the following setup:
UIViewController - this is the main, parentController
Within this main view, I have a UIScrollView at the very top, and a UITableView below it.
The UITableView is populated with core data via NSFetchedResultsController...
UIScrollView (The UIViewController is the delegate of this UIScrollView)
|- Within this, I have 2 UIViews that I show: 1 contains a UISegmentedControl, the other just contains nothing but UILabels
When the segmentedControl's value changes (ie: A user clicks on it), I just want to reload the UITableView's list of rows by issuing a new fetchRequest (by just changing the predicate and refetching) to the NSFetchedResultsController.
However, that's exactly where I began having an issue.
Whenever I issue [tableView reloadData], the top-most UIScrollView (not the UITableView's ScrollView, but the extra one I added to the UIViewController) stop responding.
Before I ever issue [tableView reloadData], scrolling works just fine. After I issue it, the scrollView stops responding, and scrolling stops completely.
I've tried all of the following:
[self.topScrollView setScrollingEnabled:YES]
self.topScrollView.delegate = self;
[self.topScrollView setContentSize:CGSizeMake(640.0f, self.topScrollView.frame.size.height)];
[self.topScrollView setContentOffset:CGPointMake(0, 0)];
I removed the UISegmentedControl completely. What I did instead was put a button at the very top of the view, and just issued [tableView reloadData];. It is when the tableView is reloaded, that the UIScrollView stops responding.
Is this a bug? Am I completely missing something here? The only thing I can even begin to think of is that the UITableView inherits from UIScrollView, and so that when the table reloads, it must be doing something to cause the topScrollView to either lose all sense of contentOffset/Size and/or its delegate or something...
Has anyone ever tried this? Has anyone ever had this issue?
The only way I could solve this was by creating a ContainerView, splitting the header out into its own UIViewController, and then setting that UIViewController as the UIVScrolLView delegate and just handling the entire header there.
While this works - I'm kind of confused, and would really like to know if a better alternative exists...
Edit: Here is a basic code example of what I was doing before I split it out the header into a ContainerView and just loaded it via embededSegue:
-(void)viewDidLoad {
[super viewDidLoad];
}
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self.topScrollView setContentSize:CGSizeMake(640.0f, self.topScrollView.frame.size.height)];
[self.topScrollView setContentOffset:CGPointMake(0, 0)];
}
// .....
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"listItemCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
// Load the cell with what is in core data at this index path..
[self configureCell:cell atIndexPath:indexPath];
return cell;
}
#pragma mark SegmentedControl Switch
- (IBAction)segmentedControlValueChanged {
NSFetchRequest *aRequest = [[self fetchedResultsController] fetchRequest];
NSPredicate *predicate;
switch(self.segmentedControl.selectedSegmentIndex) {
case 0:
predicate = [NSPredicate predicateWithFormat:#"status = %#", #"incomplete"];
break;
case 1:
predicate = [NSPredicate predicateWithFormat:#"status = %#", #"complete"];
break;
case 2:
predicate = [NSPredicate predicateWithFormat:#"status = %#", #"deleted"];
break;
default:
predicate = [NSPredicate predicateWithFormat:#"status = %#", #"none"];
break;
}
[aRequest setPredicate:predicate];
NSError *error = nil;
if (![[self fetchedResultsController] performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
[self.tableView reloadData];
// self.topScrollView.delegate = self;
// [self.topScrollView setContentSize:CGSizeMake(640.0f, self.topScrollView.frame.size.height)];
// [self.topScrollView setContentOffset:CGPointMake(0, 0)];
// [self.topScrollView setScrollEnabled:YES];
}
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