Tableview scrolling crashes app - ios

I am developing iPhone app in which i am downloading image from server in background,
Here is view of my application,
when i click on Button 1 i am fetching 5 data from server also images, after fetching data when user scrolls up i am fetching new 5 data from server again when user scrolls up i am fetching new 5 data from server and so on.
while fetching data for Button 1 if i click on Button 2 am cancelling my previous thread of Button 1 and i am fetching new 5 data for Button 2 and on scrolling it again fetching new 5 data Same as Button 1
but after some time while reloading a tableview my app gets crashes and shows:
Terminating app due to uncaught exception 'NSRangeException', reason: ' -[__NSArrayM objectAtIndex:]: index 28 beyond bounds [0 .. 4]
Here is my code snippet:
- (void)viewDidLoad
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self fetchDataForButton1];
dispatch_async(dispatch_get_main_queue(), ^(void) {
[tableView reloadData];
});
});
}
- (UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath
{
if(it is last row then fetch new 5 data)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//Code runs on background thread
[self LoadMoreData];
dispatch_async(dispatch_get_main_queue(), ^(void) {
//Code here is run on the main thread
[_tblList reloadData];
});
});
}else{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self beginBackgroundFetchTask];
[self downloadImage_3:indexPath];
[self endBackgroundFetchTask];
});
}
}
-(void)downloadImage_3:(NSIndexPath *)path{
UIImage *img = [[UIImage alloc] initWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:ImagePath]]];
if (img) {
[dicImages_msg setObject:img forKey:[[msg_array objectAtIndex:path.row] valueForKey:#"Merchant_SmallImage"]];
}
[_tblList performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:NO];
}
- (void) beginBackgroundFetchTask
{
self.backgroundFetchTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[self endBackgroundFetchTask];
}];
}
- (void) endBackgroundFetchTask
{
[[UIApplication sharedApplication] endBackgroundTask: self.backgroundFetchTask];
self.backgroundFetchTask = UIBackgroundTaskInvalid;
NSLog(#"ended BackgroundFetchTask");
}
-(void)LoadMoreData
{
//Fetches new 5 data from server...
}

I think the problem maybe at [self downloadImage_3:indexPath]; you call dispatch_async when you at Button1, and the dispatch_async block have not invoked, then you click on Button2, then I guess msg_array is cleared and filled with new 5 object, after that, dispatch_async block is invoked, the path.row for block is 28, whereas, the msg_array has new array content with 5 new objects, then crash.
You should cancel dispatch_async before click on other button, which is impossible for dispatch_async , so you can have a judgement in block:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self beginBackgroundFetchTask];
[self downloadImage_3:indexPath buttonIndex:index];
[self endBackgroundFetchTask];
});
-(void)downloadImage_3:(NSIndexPath *)path buttonIndex:(int)buttonIndex{
if(self.currentSelectIndex != buttonIndex) return; //skip reloadData if not same index
UIImage *img = [[UIImage alloc] initWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:ImagePath]]];
if (img) {
[dicImages_msg setObject:img forKey:[[msg_array objectAtIndex:path.row] valueForKey:#"Merchant_SmallImage"]];
}
[_tblList performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:NO];
}

Try this out
1) Use SDWebImage for fetching Images from Server
2) Use Pull Refresher feature for fetching next 5 data.
This will helps to fetch data in background.

Use SDWebImage for fetching Images from Server.

Disable user interaction then go to top on table:
[TableView scrollRectToVisible:CGRectMake(0, 0, 1, 1) animated:YES];
then call:
[TableView reloadData];
then enable userintercation.
hope will solve the problem

This is because images take time to load from server and cell gets no data when it is allocated.
You need to implement lazy loading for it.
Here is a good tutorial.

I would not fetch data like that from the cellForRowAtIndexPath method. That is used to display cells.
Instead do it like this:
List your original data.
When the user scrolls past the last cell, show a footer that displays a loading symbol.
at the same time While the footer is displaying a loading symbol, then and ONLY then call the fetchLoadData method, and WAIT
Until you dont hear from the server, continue displaying the loading symbol, dont start adding new cells to prepare for anything, because chances are you might either not get anymore data back, which at this point you have to get rid of the footer cell and let the user know that there is no more data to retrieve, OR if there is more data to display, THEN set up your next 5 cells by doing the following in this order:
adding your new 5 data objects to your data array
updating the number of rows in section using the numberOfRowsInSection delegate method
You will find that your cells if coded correctly will be displayed correctly once you refresh the table using [myTable reloadTable];

AS per your log array is having only 5 count and you are fetching the data as per indexpath.row and in this crash case is 28. So it might be that you are adjusting your cell indexpath row according to your new array which is fetching again and again data on scrolling but in the repetition of 5.
Just put a breakpoint and check whether you are resetting the indexpath row in count of [0 4];
UPDATED:
[[NSThread currentThread] populateDataInBackground];

Related

Proper thread handling

I've come to a problem where proper threading is needed, but I can't seem to optimise it correctly.
Here's my method:
-(void) method1
{
// -1 to an NSInteger
nsint1--;
[self showActiviyIndicator:YES]; //act as loading screen
[alloc database etc stuffs and retrieving of data here]
//for loop here to check with database, and grey out button depending on database values
for (int i = 1; i<12; i ++)
{
//get values from database and store into variables, then grey out the button if variables are 0.
}
int Val1 = [get from database]
if Val1 = 0
[button setTitleColor:[UIColor Grey]];
someLabel.text = [NSString stringWithFormat:#"%ld", (long)nsint1];
//here's where the problem lies
[self refreshTableSessionList:xx];
[self showActiviyIndicator:NO]
}
inside [self refreshTableSessionList:xx], there's a
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)
to get data from server database, then a
dispatch_async(dispatch_get_main_queue(),
to populate and reload tableViewCell.
But there'll be a conflict for when I put a
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)
before [alloc database etc stuffs and retrieving of data here]
and put dispatch_async(dispatch_get_main_queue(),
when greying out the button, but that's inside a loop, which i don't think it is the right way.
What's the solution to overcome this?
As I understood you don't wait for the finish of the background database stuff.
Have you read about multithreading? For example, Ray's article.
In a simple way, you can call dispatch_async inside the dipatch_async block inside the dispatch_async and etc.
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// do some database stuff
dispatch_async(dispatch_get_main_queue(), ^{
// do some UI stuff
});
});
So you should switch between the main thread and a global queue. Also, you can use delegates, notifications or even reactivity for such purposes.

Upload new items to table view

I have table view that load new data (depend on page) from SQL data base. Problem is, when i load it in main thread, it block UI for a while. When i try to do "hard work" in background, and reload data in main thread, odd things start to happen, for example, table view section header move in wrong place, and i load enormous amount of data.
First case, all work but block UI for while:
[self.tableView addInfiniteScrollingWithActionHandler:^{
#strongify(self)
if (!self.viewModel.isUpdating){
self.viewModel.isUpdating = YES;
[self.tableView.infiniteScrollingView startAnimating];
[self.viewModel nextPage];
[self.tableView reloadData];
self.viewModel.isUpdating = NO;
}
}];
In second case, i tried to do background work, following not work as expected:
if (!self.viewModel.isUpdating){
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//Add some method process in global queue - normal for data processing
self.viewModel.isUpdating = YES;
[self.tableView.infiniteScrollingView startAnimating];
[self.viewModel nextPage];
dispatch_async(dispatch_get_main_queue(), ^(){
//Add method, task you want perform on mainQueue
//Control UIView, IBOutlet all here
[self.tableView reloadData];
self.viewModel.isUpdating = NO;
});
//Add some method process in global queue - normal for data processing
});
}
}];
How should i modify my code to not load main thread, and without "weird" things?
have you tried something like this.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
.....
dispatch_async(dispatch_get_main_queue(), ^{
[self performSelectorOnMainThread:#selector(updateView) withObject:nil waitUntilDone:YES];
});
});
......
-(void)updateView{
[self.tableView reloadData];
self.viewModel.isUpdating = NO;
}
user PerformSelectorOnMainThread it may help you.

how to handle background task?

I am new in iOS.
I am trying to implement upload data in uitableview one by one row.
for that i am using background task.
using following code.
-(void)MethodUploadBgTaskAssign
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSString *responseString = [self MethodlblUploadClicked];
dispatch_async(dispatch_get_main_queue(), ^(void)
{
if([responseString isEqualToString:#"UploadSuccess"])
{
[self ReloadTblData];
[self ContinueUploadData];
}
});
});
}
-(void) ReloadTblData
{
if([dataArr count]>0)
[dataArr removeObjectAtIndex:0];
[uitblData reloadData];
}
-(void) ContinueUploadData
{
if((dataArr count]>0)
[self MethodUploadBgTaskAssign];
}
My problem is uploading data in table after some time table reload with empty data
because all data uploaded at that time.
I want show updated ui after uploading each cell in table.
What will be necessary changes in code?
appreciate for help.
It looks to me as if you are updating the table - but in the wrong thread (hence the table never actually appears to update). You need to use performSelectorOnMainThread to update the UI.
[self performSelectorOnMainThread:#selector(ReloadTblData:) // Update the table on the main thread
withObject:nil
waitUntilDone:NO];
I think this should work - give it a go!

UITableView with multiple sections loads choppy

I have a UITableView (TV) with several sections, each section has an NSArray that serves as the dataSource (no CoreData, no images). When the user opens the TV, my app does some intensive calculations to generate the dataSource arrays. In some cases, the calculations can take some time, and what happens then is that the section headers show first, after which the cells appear, which doesn't like good, I think.
I'm already using GCD to do the calculations:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear: animated];
[MBProgressHUD showHUDForView: self.view animated: YES];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self.model generateData];
dispatch_async(dispatch_get_main_queue(), ^{
[MBProgressHUD hideHUDForView: self.view animated: YES];
[self.tableView reloadData];
});
});
}
Besides trying to optimize the calculations, is there anything else I could do to make this look smoother? For instance, is there a way for the section headers not to appear until the calculations are done?
UPDATE:
So in the end, my solution turned out to be different. To generate my data I am now using a dispatch_group, and calculate theNSArray for each section in andispatch_group_async block, so they run concurrently. This already was an improvement in speed. Furthermore, I start the calculation already in the UIViewController from which the user opens the TV. Therefore, the data is available almost instantly when the TV opens, and all sections load smoothly.
Here is a code snippet for completeness:
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) ^{
[self.model generateArray1];
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) ^{
[self.model generateArray2];
});
//... etc for each section
// make sure that everything is done before moving on
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
If you return nil from tableView:titleForHeaderInSection: then the header won't be shown, so add a small amount of conditional logic which checks if the data is loaded yet and either returns nil (if not loaded) or the section title (if it is loaded).

UITableView not being displayed after reloading data

I have a strange problem with my tableView.
I load data via JSON into my tableView. While the JSON is being requested from the web in another class, I show an activity indicator view in my current view and the tableView is hidden.
I ve got a delegate method, which is called as soon as the json is ready.
-(void)didReceivePlayers:(NSArray *)players {
[activityIndicator stopAnimating];
tableViewPlayers.hidden = false;
startButton.hidden = false;
playersData = [[NSMutableArray alloc] initWithArray:players];
[tableViewPlayers reloadData];
NSLog(#"done reloading");
}
The method is being called perfectly.
The code is pretty straight forward. I hide my activity indicator and show my tableView.
Then I call reloadData. It takes only a few milliseconds. BUT after reloadData, my activityIndicator is still shown and it takes several seconds to show my tableview, although the nslog is being called right away.
I also tried calling reload data in mainThread, but this did not change a thing.
Thanks for your help!
Be sure that the code is being executed on the main thread. You can use the main operation queue like this:
-(void)didReceivePlayers:(NSArray *)players {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[activityIndicator stopAnimating];
tableViewPlayers.hidden = false;
startButton.hidden = false;
playersData = [[NSMutableArray alloc] initWithArray:players];
[tableViewPlayers reloadData];
NSLog(#"done reloading");
}];
}

Resources