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;
}
Related
I struggle with tableview datasource and delegate methods. I've a tableview if user navigate to that table view controller I'm calling web service method which is block based service after the request finished successfully reloaded tableview sections but tableView:cellForRowAtIndexPath:indexPath called twice. Here's my code.
viewDidLoad
[[NetworkManager sharedInstance].webService getValues:self.currentId completion:^(NSArray *result, BOOL handleError, NSError *error) {
self.data = result[0];
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 4)] withRowAnimation:UITableViewRowAnimationAutomatic];
});
}
but cellForRowAtIndexPath self.data value is null in the first time.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSLog(#"%#", self.data); // first time it print null
}
So is there any ideas about this? Thank you so much!
Did you initialise the data array in viewDidLoad? If you didn't it will return null.
if you want to avoid two calls to the tableview try this:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
if(!data)
return 0;
return yourNumberOfSections;
}
The tableview calls cellForRowAtIndexPath when it needs to render a cell on screen. If the tableview appears before it has any data, self.data will be empty.
In viewDidLoad set [[self.data = NSMutableArray alloc] init] (for example) and in the datasource/delegate methods of UIITableView it should correctly return numberOfRows etc as zero until your web service populates your data.
It sounds like -tableView:cellForRowAtIndexPath: is being called simply because the view is loaded and the table is trying to populate itself before you've received any data from your web service. At that point, you won't have set self.data (whatever that is) to any useful value, so you get null instead. When your web service returns some data, the completion routine causes the relevant sections to be reloaded, and the table draws the data.
I have implemented an UITableView with load more functionality. The tableView loads big images from a sometimes slow server. I'm starting an URLConnection for each image and reload the indexPath corresponding to the URLConnection (saved with the connection object). The connections themselves call -reloadData on the tableView.
Now when clicking the load more button, I scroll to the first row of the new data set with position bottom. This works great and also my asynchronous loading system.
I faced the following issue: When the connection is "too fast", the tableView is reloading the data at a given indexPath while the tableView is still scrolling to the first cell of the new data set, the tableView scrolls back half the height of that cell.
This is what it should look like and what it actually does:
^^^^^^^^^^^^ should ^^^^^^^^^^^^ ^^^^^^^^^^^^^ does ^^^^^^^^^^^^^
And here is some code:
[[self tableView] beginUpdates];
for (NSMutableDictionary *post in object) {
[_dataSource addObject:post];
[[self tableView] insertRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:[_dataSource indexOfObject:post] inSection:0]] withRowAnimation:UITableViewRowAnimationBottom];
}
[[self tableView] endUpdates];
[[self tableView] scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:[_dataSource indexOfObject:[object firstObject]] inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:YES];
-tableView:cellForRowAtIndexPath: starts a JWURLConnection if the object in the data source array is a string, and replaces it with an instance of UIImage in the completion block. Then it reloads the given cell:
id image = [post objectForKey:#"thumbnail_image"];
if ([image isKindOfClass:[NSString class]]) {
JWURLConnection *connection = [JWURLConnection connectionWithGETRequestToURL:[NSURL URLWithString:image] delegate:nil startImmediately:NO];
[connection setFinished:^(NSData *data, NSStringEncoding encoding) {
[post setObject:[UIImage imageWithData:data] forKey:#"thumbnail_image"];
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}];
[cell startLoading];
[connection start];
}
else if ([image isKindOfClass:[UIImage class]]) {
[cell stopLoading];
[cell setImage:image];
}
else {
[cell setImage:nil];
}
Can I prevent the tableView from performing the -reloadRowsAtIndexPaths:withRowAnimation: calls until the tableView scrolling is done? Or can you imagine a good way to prevent this behavior?
Based on the ideas of Malte and savner (please upvote his answer as well) I could implement a solution. His answer didn't do the trick, but it was the right direction.
I had to implement -scrollViewDidEndScrollingAnimation:. I created a bool property called _autoScrolling and an NSMutableArray property for the index paths that got reloaded while scrolling. In the URLConnections finish block I did this:
if (_autoScrolling) {
if (!_indexPathsToReload) {
_indexPathsToReload = [NSMutableArray array];
}
[_indexPathsToReload addObject:indexPath];
}
else {
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
And then this:
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
[self performSelector:#selector(performRelodingAfterAutoScroll) withObject:nil afterDelay:0.0];
}
- (void)performRelodingAfterAutoScroll {
_autoScrolling = NO;
if (_indexPathsToReload) {
[[self tableView] reloadRowsAtIndexPaths:_indexPathsToReload withRowAnimation:UITableViewRowAnimationFade];
}
_indexPathsToReload = nil;
}
It took me quite a long time to find the trick with -performSelector:withObject:afterDelay: and I still don't know why I need it.
I thought the method might got called too early. So I implemented a delay of a second and tried how far I can take it down. It still works with 0.0 but not if I call the method directly or use -performSelector:withObject:.
I really hope someone can explain that.
EDIT
After revisiting this a few years later I can explain what's going on here:
Calling -[NSObject (NSDelayedPerforming) performSelector:withObject:afterDelay:] guarantees the call to be performed in the next runloop iteration.
So an even better or IMHO more beautiful solution would be:
[[NSOperationQueue currentQueue] addOperationWithBlock:^{
[self performRelodingAfterAutoScroll];
}];
I wrote a more detailed explanation in this answer.
Sorry i don't have enough reputation to add a comment, hence the answer to your last question in a separate answer.
-performSelector:withObject:afterDelay: with a delay of 0.0 seconds does not execute the given selector immediately but instead performs it after the current Runloop Cycle finishes and after the given delay.
Where as -performSelector:withObject: is added to and executed in the current Runloop Cycle. Which is the same as directly calling the method.
Therefore using -performSelector:withObject:afterDelay: the UI will get updated in the current Runloop Cycle i.e in this case the scrolling animation can finish, before your selector is performed(and reloads the UI once more).
Source: Apple Dev Docs and this Thread Answer
You can use the UIScrollViewDelegate protocols (which you get for free using UITableViewDelegate) and utilize the -scrollViewDidScroll or -scrollViewWillBeginDragging: methods to detect scrolling has started or stopped. Work with those callbacks to control when you want to load/stop loading cell data.
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.
I'v got a UITableView whose dataSource updated at random intervals in a very short period of time. As more objects are discovered, they are added to the tableView's data source and I insert the specific indexPath:
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView endUpdates];
The data source is located in a manager class, and a notification is posted when it changes.
- (void)addObjectToDataSource:(NSObject*)object {
[self.dataSource addObject:object];
[[NSNotificationCenter defaultCenter] postNotification:#"dataSourceUpdate" object:nil];
}
The viewController updates the tableView when it receives this notification.
- (void)handleDataSourceUpdate:(NSNotification*)notification {
NSObject *object = notification.userInfo[#"object"];
NSIndexPath *indexPath = [self indexPathForObject:object];
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView endUpdates];
}
This works fine, but I noticed that in some cases, a second object is discovered just as the first one is calling endUpdates, and I get an exception claiming I have two objects in my data source when the tableView was expecting one.
I was wondering if anyone has figured out a better way to atomically insert rows into a tableView. I was thinking of putting a #synchronized(self.tableView) block around the update, but I'd like to avoid that if possible because it is expensive.
The method I've recommended is to create a private queue for synchronously posting batch updates onto the main queue (where addRow is a method that inserts an item into the data model at a given indexPath):
#interface MyModelClass ()
#property (strong, nonatomic) dispatch_queue_t myDispatchQueue;
#end
#implementation MyModelClass
- (dispatch_queue_t)myDispatchQueue
{
if (_myDispatchQueue == nil) {
_myDispatchQueue = dispatch_queue_create("myDispatchQueue", NULL);
}
return _myDispatchQueue;
}
- (void)addRow:(NSString *)data atIndexPath:(NSIndexPath *)indexPath
{
dispatch_async(self.myDispatchQueue, ^{
dispatch_sync(dispatch_get_main_queue(), ^{
//update the data model here
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView endUpdates];
});
});
}
By doing it this way, you don't block any other threads and the block-based approach ensures that the table view's animation blocks (the ones that are throwing the exceptions) get executed in the right order. There is a more detailed explanation in Rapid row insertion into UITableView causes NSInternalInconsistencyException.
This is the problem: I update my tableview after saving and fetching an thumbnail to Core Data and then I tell the cell to update itself - so it can show a thumbnail when the image has been loaded into Core Data. I Use two different threads as Core Data is not threadsafe and all GUI elements of course needs to happen in the main thread.
But this whole method just keep looping forever, and what is causing it is when I reload the thread:
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
Why? and how do I fix this?
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Photo"];
Photo *photo = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = photo.title;
cell.detailTextLabel.text = photo.subtitle;
NSLog(#"Context %#", self.photographer.managedObjectContext);
[self.photographer.managedObjectContext performBlock:^{
[Photo setThumbnailForPhoto:photo];
dispatch_async(dispatch_get_main_queue(), ^{
cell.imageView.image = [UIImage imageWithData:photo.thumbnail];
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
});
}];
return cell;
}
The infinite loop is caused by calling the cellForRowAtIndexPath: again after the fetch is loaded and the reload on a certain cell is called.
A reload will force the cellForRowAtIndexPath: to be called again... and in your case again and again to infinity.
The solution is simple... Do not reload your cell in the cellForRowAtIndexPath but rather in a callback-method of the fetchrequest. Then reload it there rather then the creation of the cell.
Rather do not load the image inside the cellForRowAtIndexpath: at all.
Whenever your table is instantiated make a method that loops over your datasource and get the respective cell for each item. Then load an image for each cell you deem needed. And reload the cell whenever the fetching of the item is done (for instance a callback method).
If you do want the image to be loaded inside the creation of the cell as you have done now (Although I don't think that is the proper way to do it). You can surround the whole performBlock: with an if-statement checking if the image has already been set or not.
As already been said by others, there should be no need to call reloadRowsAtIndexPaths when the image has been loaded. Assigning a new image to cell.imageView.image is sufficient.
But there is another problem with your code. I assume that self.photographer.managedObjectContext is a managed object context of the "private concurrency type", so that performBlock executes on a background thread. (Otherwise there would be no need to use dispatch_async(dispatch_get_main_queue(), ...).)
So performBlock: executes the code asynchronously, on a background thread. (Which is good because the UI is not blocked.) But when the image has been fetched after some time, the cell might have been reused for a different row. (This happens if you scroll the table view so that the row becomes invisible while the image is fetched.)
Therefore, you have to check if the cell is still at the same position in the tableview:
[self.photographer.managedObjectContext performBlock:^{
[Photo setThumbnailForPhoto:photo];
dispatch_async(dispatch_get_main_queue(), ^{
if ([[tableView indexPathForCell:cell] isEqualTo:indexPath]) {
cell.imageView.image = [UIImage imageWithData:photo.thumbnail];
}
});
}];
I'd replace the following code:
cell.imageView.image = [UIImage imageWithData:photo.thumbnail];
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
With:
UITableViewCell *blockCell = [tableView cellForRowAtIndexPath:indexPath];
blockCell.imageView.image = [UIImage imageWithData:photo.thumbnail];
[blockCell setNeedsLayout];
This solves the issue of reloading recursively, as well as the possibility that cell has gotten reused in the interim. I'd also check for whether or not you've loaded the photo data already and not update if so.
Since you are using an NSFetchedResultsController though, you may be better off going with Totomus Maximus' answer, if the delegate callbacks are already informing you when the photo data updates. This way just updates the image view and wouldn't update any other information that the Photo update method could change.