Loading data to use in UITableView cellForRowAtIndexPath - ios

I have a UITableView and I am populating it with image data. This data is loaded fine, but when the records increase in number (e.g. more than 50) the app starts to have problems like freezes and more. I understood that this is the line in my cellForRowAtIndexPath that is causing the issue:
NSData* data = [DKStoreManager loadFileWithName:[finalArray objectAtIndex:indexPath.row] forFolderNumber:[_FolderCode intValue] forUser:[_PassedUserID intValue] andType:_FolderType];
NSDictionary *myDictionary = (NSDictionary*) [NSKeyedUnarchiver unarchiveObjectWithData:data];
I understood it because when I loaded the data only once in the viewDidLoad an used myDictionary as a global variable, then all the cells where logically be the same, but the table scrolled fine fine and the app doesn't crash. finalArray is an array with the names of the files ordered in alphabetical order and the number of rows corresponds to its count. Can anyone suggest a way to load this data outside of the cellForRowAtIndexPath method? How do I then pass everything on to the cellForRowAtIndexPath if all of the NSData are different?
What I have tried to do:
1) I tried to subclass the UITableViewCells and load the data from a method:
cell.FileName = [finalArray objectAtIndex:indexPath.row];
cell.PassedUserID = _PassedUserID;
cell.FolderCode = _FolderCode;
cell.FolderType = _FolderType;
[cell loadContents];
I made sure using a BOOL that loadContents runs only once in the subclass. When I scroll down or up, cells change position. Its a mess...
2) I noticed that if I remove the
if (cell == nil) {
and stop reusing the cell, there are no issues with the cells changing place, but there are huge loading time issues
2) Moving everything in the if (cell == nil) { method, the cells still change place on scroll but the scroll is faster...
4) Loading all the data in the viewDidLoad displaying a "loading..." but the loading is really slow, it doesn't really work out.
5) Using dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ on the data load, but doesn't work, the table scrolls really slow...
PS ,myDictionary contains the image and the name of the cell.
EDIT:
All the data is saved locally, the method "loadFileWithName" loads a saved file in the documents directory.

Please see example of lazy-loading table below.
This example provides a table view with infinite number of cells; data for each cell is loaded in a background thread only when all of the following conditions are true:
UITableView requests that cell;
Cell that data is requested for is visible;
UITableView is not scrolling OR scrolling is not decelerated (i.e. scrolling is performed while user’s finger touches the screen).
That is, excessive load of the system during fast scrolling is eliminated, and data is loaded only for cells that user really needs to see.
In this example, time-consuming and thread-blocking data loading operation is emulated for each cell with sleeping a background thread for 0.2 seconds. To use this example in your real application please do the following:
Replace implementations of the tableElementPlaceholder getter;
In the performActualFetchTableCellDataForIndexPaths: method, replace the following line with your actual loading cell data:
[NSThread sleepForTimeInterval:0.2]; // emulation of time-consuming and thread-blocking operation
Tune implementation of the tableView:cellForRowAtIndexPath: method for your cells implementation.
Please note that any object data needed in the loading code should be thread-safe since loading is performed in non-main thread (i.e. atomic properties and probably NSLock should be used inside the performActualFetchTableCellDataForIndexPaths: method in your time-consuming thread blocking code replacing the [NSThread sleepForTimeInterval:0.2] call).
You can download the full Xcode project from here.
#import "ViewController.h"
#import "TableViewCell.h"
static const NSUInteger kTableSizeIncrement = 20;
#interface ViewController () <UITableViewDataSource, UITableViewDelegate>
#property (nonatomic, strong) NSMutableArray* tableData;
#property (weak, nonatomic) IBOutlet UITableView *tableView;
#property (nonatomic, readonly) id tableElementPlaceholder;
#property (nonatomic, strong) NSTimer* tableDataLoadDelayTimer;
- (void)fetchTableCellDataForIndexPath:(NSIndexPath*)indexPath;
- (void)performActualFetchTableCellDataForIndexPaths:(NSArray*)indexPaths;
- (void)tableDataLoadDelayTimerFired:(NSTimer*)timer;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.tableData = [NSMutableArray arrayWithCapacity:kTableSizeIncrement];
for (NSUInteger i = 0; i < kTableSizeIncrement; i++) {
[self.tableData addObject:self.tableElementPlaceholder];
}
// Do any additional setup after loading the view, typically from a nib.
}
- (id)tableElementPlaceholder
{
return #"";
}
- (void)fetchTableCellDataForIndexPath:(NSIndexPath*)indexPath
{
if (self.tableView.decelerating && !self.tableView.tracking) {
if (self.tableDataLoadDelayTimer != nil) {
[self.tableDataLoadDelayTimer invalidate];
}
self.tableDataLoadDelayTimer =
[NSTimer scheduledTimerWithTimeInterval:0.1
target:self
selector:#selector(tableDataLoadDelayTimerFired:)
userInfo:nil
repeats:NO];
} else {
[self performActualFetchTableCellDataForIndexPaths:#[indexPath]];
}
}
- (void)tableDataLoadDelayTimerFired:(NSTimer*)timer
{
[self.tableDataLoadDelayTimer invalidate];
self.tableDataLoadDelayTimer = nil;
NSArray* indexPathsForVisibleRows = [self.tableView indexPathsForVisibleRows];
[self performActualFetchTableCellDataForIndexPaths:indexPathsForVisibleRows];
}
- (void)performActualFetchTableCellDataForIndexPaths:(NSArray*)indexPaths
{
for (NSIndexPath* indexPath in indexPaths) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
[NSThread sleepForTimeInterval:0.2]; // emulation of time-consuming and thread-blocking operation
NSString* value = [NSString stringWithFormat:#"Text at cell #%ld", (long)indexPath.row];
dispatch_async(dispatch_get_main_queue(), ^{
self.tableData[indexPath.row] = value;
[self.tableView reloadRowsAtIndexPaths:#[indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
});
});
}
}
#pragma mark UITableViewDataSource protocol
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.tableData.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row == (self.tableData.count - 1)) {
for (NSUInteger i = 0; i < 20; i++) {
[self.tableData addObject:self.tableElementPlaceholder];
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
}
TableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"TableViewCell" forIndexPath:indexPath];
NSString* text = [self.tableData objectAtIndex:indexPath.row];
if (text.length == 0) {
cell.activityIndicator.hidden = NO;
[cell.activityIndicator startAnimating];
cell.label.hidden = YES;
[self fetchTableCellDataForIndexPath:indexPath];
} else {
[cell.activityIndicator stopAnimating];
cell.activityIndicator.hidden = YES;
cell.label.hidden = NO;
cell.label.text = text;
}
return cell;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
#end
For your project, the performActualFetchTableCellDataForIndexPaths: method would look as follows:
- (void)performActualFetchTableCellDataForIndexPaths:(NSArray*)indexPaths
{
for (NSIndexPath* indexPath in indexPaths) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
NSData* data = [DKStoreManager loadFileWithName:[finalArray objectAtIndex:indexPath.row] forFolderNumber:[self.FolderCode intValue] forUser:[self.PassedUserID intValue] andType:self.FolderType];
NSDictionary *myDictionary = (NSDictionary*) [NSKeyedUnarchiver unarchiveObjectWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
self.tableData[indexPath.row] = myDictionary;
[self.tableView reloadRowsAtIndexPaths:#[indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
});
});
}
}
Please note that you'll need to use atomic properties self.FolderCode and self.PassedUserID instead of instance variables _FolderCode and _PassedUserID, because loading file is performed in a separate thread and you need to make this data thread-safe.
As for the tableElementPlaceholder method, it might look as follows:
- (id)tableElementPlaceholder
{
return [NSNull null];
}
Correspondingly, in the tableView: cellForRowAtIndexPath: method check if data load completed would look like this:
NSObject* data = [self.tableData objectAtIndex:indexPath.row];
if (data == [NSNull null]) {
cell.activityIndicator.hidden = NO;
[cell.activityIndicator startAnimating];
cell.label.hidden = YES;
[self fetchTableCellDataForIndexPath:indexPath];
} else if ([data isKindOfClass:[NSData class]]) {
[cell.activityIndicator stopAnimating];
cell.activityIndicator.hidden = YES;
NSData* actualData = (NSData*)data;
// Initialize your cell with actualData...
}

Not a good idea
Loading a lot of cells on a tableView it's not a good way to display data. Let suppose if a user wants to take a look at your data, he will take a look at your first elements (suppose max 20 cells), why he should drag to see elements all the way down? If he wants more he just goes down and clicks load more. User doesn't want to keep waiting and loading all the data to the memory (RAM).
Suggestion
For me the best way is to add a load more function (also called infinite scrolling) when you go to the bottom of your loaded elements (it's the same way as pagination on websites).
Implementation
There is a great library to achieve this thing called SVPullToRefresh. This library allow you by draging down the tableView to trigger a method (completionHandler) where you should load more data at the end of tableView.
//This is where we add infinite scrolling to the tableView
__weak YourViewController *weakSelf = self;
[self.tableView addInfiniteScrollingWithActionHandler:^{
[weakSelf loadMoreData];
[weakSelf.tableView reloadData];
}];
Now that we added load more, we need to setup our data. First we declare a NSInteger *lastIndex; as an instance variable, an inthe viewWillAppear we instantiate it with 0, and call loadMoreData which will give the lastIndex the value 20
- (void)viewWillAppear:(BOOL)animated {
lastIndex = 0;
[self loadMoreData];
}
This is the method that takes care of data from Array to tableView. Here we are describing an example which gets 20 elements for load more pressed.
-(void)loadMoreData {
//Lets suppose we are loading 20 elemets each time
//Here we control if we have elements on array
if(yourDataArray.count - lastIndex>0) {
//We control if we have 20 or more so we could load all 20 elements
if(yourDataArray.count - lastIndex>=20) {
lastIndex += 20;
} else {
//Our last index is the last element on array
lastIndex = yourDataArray.count - lastIndex;
}
} else {
//We have loaded all the elements on the array so alert the user
}
}
In numberOfRowsInSection we return the lastIndex
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return lastIndex;
}
Conclusions
To deal with images you should use SDWebImage as the other answers suggested. Dealing the problem of scrolling this way, you eleminate the way to add large amount of data on tableViews at the first look at it. Maybe it's not the right answer to deal with your idea (if you really need to load all data to your tableView), but it tells you how to avoid loading data at giving you performance at scrolling to your tableView.
p.s I am using this example in my Social Networking App and it's working great. (Also Instagram, Facebook and other apps are dealing with loading data this way).
Hope it helps :)

I have seen similar approaches as yours in the past years and they all have been wrong. If I were you I would use 2 different approaches:
1) If you load images from a remote server use a great library called SDWebImage or write your one of own. It's going to get as simple as just [self.imgViewBack setImageWithURL:[NSURL URLWithString:data.eventImageURL];
2) You could also save all your images to NSDocumentDirectory and while them stored there - just use an NSDictionary of paths.
My general recommendation here goes as follows - never create an array or dictionary of raw data of objects the size more than a few kilobytes. This will result in terrible freezes and memory waste all over the place. Always use paths to the object and store them somewhere else - and you will benefit.
Generally cellForRowAtIndexPath method is the most frequently used in UITableViewDataSource protocol so it should be executed with lightning fast speed.

If you have to store everything locally and not on a server as SergiusGee said, I would suggest keeping, for example, only 50 images in memory.
There's the tableView:willDisplayCell:forRowAtIndexPath: delegate method, where you could control which elements are loaded.
For example, when displaying the 50th cell, you have images between index 30-70. After every 10th cell, you could unload the unneeded ones and reload new ones to keep a [x-20, x+30] range, where x is the current cell.
Obviously this will take quite some effort to implement, but loading everything and keeping it in memory is certainly not the answer.
Also, the numbers are just for the examples sake.

Do not load the data from file in -cellForRowAtIndexPath:.
Try loading them into an array of UIImage in -viewDidLoad: synchronously or async-ly (which is up to you) because it does not take too much time for UIImage to be presented but it is extremely expensive to load raw data to construct an UIImage.

Related

UICollectionView is not reloading data properly

I'm using a UICollectionView to display some data received from a remote server and I'm making two reloadData calls in a short time, first one to show a spinner (which is actually a UIActivityIndicatorView in a UICollectionViewCell) and the second one after the data was retrieved from server. I store the models based on which the collection view cells are created in a self.models NSArray and in cellForItemAtIndexPath I dequeue cells based on this model. My issue is that when I call reloadData after the request was completed (the second time) the collection view seems to be still in the process of creating the cells for the first reloadData call and it shows just the first two cells and a big blank space in place where the rest of the cells should appear.
I wrote some logs to get some insights of what's happening and the current workflow is:
I'm populating self.models with the model for the spinner cell and then I call reloadData on the UICollectionView.
numberOfItemsInSection is called, which is just returning [self.models count]. The returned value is 2, which is fine (one cell which acts like a header + the second cell which is the one with the UIActivityIndicator inside).
I'm making the request to the server, I get the response and I populate self.models with the new models received from the remote server (removing the model for the spinner cell but keeping the header cell). I call reloadData on the UICollectionView instance.
numberOfItemsInSection is called, which is now returning the number of items retrieved from the remote server + the model for the header cell (let's say that the returned value is 21).
It's just now when cellForItemAtIndexPath is called but just twice (which is the value returned by numberOfItemsInSection when it was called first).
It seems like the collection view is busy with the first reloadData when I call this method for the second time. How should I tell the collection view to stop loading cells for the first reloadData call? I tried with [self.collectionView.collectionViewLayout invalidateLayout] which seemed to work but the issue reappeared, so it didn't did the trick.
Some snippets from my code:
- (void)viewDidLoad {
[super viewDidLoad];
[ ...... ]
self.models = #[];
self.newsModels = [NSMutableArray array];
[self.collectionView.collectionViewLayout invalidateLayout];
[self buildModel:YES]; // to show the loading indicator
[self.collectionView reloadData];
[self updateNewsWithBlock:^{
dispatch_async(dispatch_get_main_queue(), ^{
[self.collectionView.collectionViewLayout invalidateLayout];
[self buildModel:NO];
[self.collectionView reloadData];
});
}];
}
- (void) buildModel:(BOOL)showSpinnerCell {
NSMutableArray *newModels = [NSMutableArray array];
[newModels addObject:self.showModel]; // self.showModel is the model for the cell which acts as a header
if ([self.newsModels count] != 0) {
// self.newsModels is populated in [self updateNewsWithBlock], see below
[newModels addObjectsFromArray:self.newsModels];
} else if (showSpinnerCell) {
[newModels addObject:[SpinnerCellModel new]];
}
self.models = [NSArray arrayWithArray:newModels];
}
- (void) updateNewsWithBlock:(void (^)())block {
// Here I'm performing a GET request using `AFHTTPSessionManager` to retrieve
// some XML data from a backend, then I'm processing it and
// I'm instantiating some NewsCellModel objects which represents the models.
[ ..... ]
for (NSDictionary *item in (NSArray*)responseObject) {
NewsCellModel *model = [[NewsCellModel alloc] init];
model.itemId = [item[#"id"] integerValue];
model.title = item[#"title"];
model.headline = item[#"short_text"];
model.content = item[#"text"];
[self.newsModels addObject:model];
}
block();
}

Asynchronous loading of data from multiple api

Loading data from one api request and storing it in one array (suppose n object coming in response json object),
and another api request takes argument from first api request and loads status of n objects.
1) first api request will load n objects and display it into table:
dispatch_queue_t loadDataQueue = dispatch_queue_create("loadDataQueue",NULL);
dispatch_async(loadDataQueue, ^{
// Perform long running process
[self loadData];
dispatch_async(dispatch_get_main_queue(), ^{
// Update the UI
[tableView reloadData];
[self hideActivityView];
});
});
2) now I am calling loadstatus method, it takes parameter from objectatindex and loads status data for objectatindex. So this method calling n times in cellForRowAtIndexPath method.
dispatch_queue_t loadStatusQueue = dispatch_queue_create("loadStatusQueue",NULL);
dispatch_async(loadStatusQueue, ^{
// Perform long running process
[self loadStatus];
dispatch_async(dispatch_get_main_queue(), ^
// Update the UI
[tableView reloadData];
});
});
This updates one row at a time. So reloading table n times.
It takes so time to load status of all objects.
Some time hanging problem occured.
Can somebody please provide efficient solution for this or other way to do this?
The info in your question was a bit unclear ( especially about objectAtIndex... Does it mean that in cellForRowAtIndexPath you get the cell's info, then based on that you send another async request to get it's status?)
It might not be enough to tell all reasons could make your app slow, but I could say it's not a good idea to reload the whole table just to update one cell. Also I think you should call loadData to get full data list of basic info first, then call loadStatus for 'visible' cells.
I guess you know how to store the loaded data/status in arrays to prevent refetching data. So the example below is possibly what you could adopt to improve the performance:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:#"ReusableCell" forIndexPath:indexPath];
[cell configureData:self.loadedData[indexPath.row]];
if (self.loadedStatus[indexPath.row]) {
// If the status has been loaded then
[cell configureStatus:self.loadedStatus[indexPath.row]];
} else {
dispatch_queue_t loadStatusQueue = dispatch_queue_create("loadStatusQueue",NULL);
__weak __typeof(self) weakSelf = self;
dispatch_async(loadStatusQueue, ^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
// Perform your long running process here
// Eg: [strongSelf loadStatusForIndex:indexPath.row];
UITableViewCell *blockCell = (UITableViewCell *)[strongSelf.tableView cellForRowAtIndexPath:indexPath];
dispatch_async(dispatch_get_main_queue(), ^{
[strongSelf.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
});
}
});
....
}
return cell;
}

How to ensure remotely-fetched images in UITableView cells always filled?

I have gotten this question several times in job interviews: You have a UITableView and the user is scrolling very fast. How exactly do you make sure at all of the cells' images which are being loaded from a server are visible wherever the user scrolls to.
I can think of several techniques. For instance what I have done is use an operation queue, loaded with requests to fetch images, and when the user starts scrolling, empty the queue and fill it with requests for images wherever the user is going towards.
Another solution is to fill the images with super-low-resolution thumbnails e.g. a 4-point gradient so that some image, albeit a bad one, is there long before the real image arrives.
I would say that the key here is "wherever the user scrolls to". Because of that, you shouldn't start downloading images until you know which cells are going to be visible.
So what I would do is watch for the UIScrollViewDelegate call:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
At that point, get the cells visible using:
- (NSArray *)indexPathsForVisibleRows
And then spin off my downloads for images. Putting it all together:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
NSArray *theIndexPaths = [self.tableView indexPathsForVisibleRows];
for (NSIndexPath *theIndexPath in theIndexPaths) {
SomeObject *myObject = self.listOfMyObjects[theIndexPath.row];
[myObject downloadImage];
}
}
This is very basic. Obviously need to update the cell, etc.
I would say "empty the queue and fill it with requests for images wherever the user is going towards" is correct using indexPathsForVisibleRows. But the images will never appear immediately unless they are very small.
I think what they mean is to show the downloaded image immediately after it has been downloaded from the background thread, which would be: dispatch_get_main_queue()
In .h file:
#interface ViewController : UIViewController <UIScrollViewDelegate>
In .m file:
- (void)downloadVisibleRowImages
{
NSArray *visibleRows = [self.tableView indexPathsForVisibleRows];
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
for (NSIndexPath *visibleRow in visibleRows) {
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath: visibleRow];
NSString *imageUrl = [_responseImages objectAtIndex:visibleRow.row];
UIImage *image = nil; // Download image here
dispatch_async(dispatch_get_main_queue(), ^{ // use main thread to display image immediately after download
if (image) [cell.imageView setImage:image];
});
}
});
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
if (!decelerate) {
[self downloadVisibleRowImages];
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
[self downloadVisibleRowImages];
}

UITableview in UITableviewcontroller - cellforrowatindexpath not called on reload

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!

Get notified when UITableView has finished asking for data?

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

Resources