UITableView with multiple sections loads choppy - ios

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).

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.

Reload data of UITableView in background

In my app, I have a UITableViewController.
Its tableView is divided in 3 sections.
I download datas for each of those sections from my server. To do this, I have 3 functions (for example f1 f2 and f3). Each updates a corresponding NSArray, used as data source for my table.
Now what I want is to reload datas using this functions and refresh my tableView once this 3 functions are done, but without disturbing the user.
I'm not used with asynchronous request, blocks, threads etc... and I'm looking for tips.
Actually, here is what I do :
-(void)viewDidLoad
{
//some settings
[NSTimer scheduledTimerWithTimeInterval:15.0 target:self selector:#selector(reloadDatas) userInfo:nil repeats:YES];
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
[self reloadDatas];
});
}
-(void)reloadDatas
{
dispatch_queue_t concurrentQueue = dispatch_get_main_queue();
dispatch_async(concurrentQueue, ^{
[self f1];
[self f2];
[self f3];
[myDisplayedTable reloadData];
});
}
-(void)f1
{
//load datas with a url request and update array1
}
-(void)f2
{
//load datas with a url request and update array2
}
-(void)f3
{
//load datas with a url request and update array3
}
But here, my tableView is "frozen" until it is refreshed.
I don't care about the order of execution of f1 f2 and f3, but I need to wait for this 3 functions to be done before refresh my tableView.
Thanks for your help.
EDIT
Thanks for all your answers.
Here is the working solution :
As mros suggets, I removed the dispatch queue from the viewDidLoad, and replace in reloadDatas:
dispatch_queue_t concurrentQueue = dispatch_get_main_queue();
with
dispatch_queue_t mainThreadQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
And finally, I reload my table in a main thread
dispatch_async(dispatch_get_main_queue(), ^{ [myDisplayedTable reloadData]; });
So your "background thread" is actually your main thread. You have to use dispatch_get_global_queue and specify a priority to actually get a different thread. Also, the dispatch async in viewDidLoad is useless as all view controller lifecycle methods are called in the main thread. I would recommend doing something as follows in your f1, f2 and f3 methods:
Start by launching an asynchronous url request, then in the completion block, update arrayX and reload a particular section of your tableview. This way all three requests can happen simultaneously and the table just updates the necessary data when each one finishes. Alternatively, if you only want to reload once, just replace the concurrentQueue variable you have with a background thread and then perform [tableView reloadData] on the main thread.
The previous answers are absolutely right. However your implementation of reloadDatas & viewDidLoad is a bit problematic.
Just to clarify:
You want to complete the time consuming data loading stuff in a background thread, then update the UI/Cells when your data is ready on the main thread.
Like so:
-(void)viewDidLoad
{
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.my.backgroundQueue", NULL);
dispatch_async(concurrentQueue, ^{
[self reloadDatas];
});
}
-(void)reloadDatas
{
// Expensive operations i.e pull data from server and add it to NSArray or NSDictionary
[self f1];
[self f2];
[self f3];
// Operation done - now let's update our table cells on the main thread
dispatch_queue_t mainThreadQueue = dispatch_get_main_queue();
dispatch_async(mainThreadQueue, ^{
[myDisplayedTable reloadData]; // Update table UI
});
}
One other thing. Pulling data from a server and updating table cells is pretty common.
No need for queues or timers here.
Here's an alternative structure.
Say you're pulling mp3's from your server :
Your model class is : Music.h/m
Your Model manager is : MusicManager.h/m (Singleton) - it will contain an array of music objects - that singleton is basically your datasource;
and finally your UItableViewController : MusicTableVC.h/m
In MusicManager.h/m : You have an NSMutableArray which will be loaded with Music.h objects that you've pull from the server. You can do that as soon as you app loads without even waiting for the TableViewController.
Inside MusicManager you have a few helper methods to add or remove items from the mutableArray and provide the count and of course your networking methods.
Finally : Post a notification in your network code. Your UITableViewController should listen/observe that notification and "reload" accordingly.
[[NSNotificationCenter defaultCenter] postNotificationName:#"NewMusicAdded" object:nil];
You query data from your server, parse the data into Music objects add them to your NSMutable array and post a notification to let the table update itself.
Pretty standard recipe.
In reloadDatas method you should change this line:
dispatch_queue_t concurrentQueue = dispatch_get_main_queue();
To:
dispatch_queue_t concurrentQueue = dispatch_queue_create("some queue", NULL);
But when you call [myDisplayedTable reloadData], you need to call this operation in the main queue.
dispatch_async(dispatch_get_main_queue(), ^{ [myDisplayedTable reloadData]; });

GCD, order of execution?

Assume we have one UIVewcontroller, call it A, in the viewdidload of that VC we add to it two UIViewcontrollers( B,C ). now to make the UI smooth in the Viewdidload of A we do some GCD work
dispatch_queue_t queue = dispatch_queue_create("CustomQueue", NULL);
dispatch_async(queue, ^{
// Create views, do some setup here, etc etc
// Perform on main thread/queue
dispatch_async(dispatch_get_main_queue(), ^{
// this always has to happen on the main thread
[self.view addSubview:myview1];
[self.view addSubview:myview2];
[self.view addSubview:myview3];
});
});
Now based on this code, am I guaranteed that the views will be added in the same order? view 1 , then 2 , then 3?
I am noticing that arbitrarily some views shows up before others !!
Your problem is almost certainly this part:
dispatch_async(queue, ^{
// Create views, do some setup here, etc etc
You cannot do anything view-related (or really anything UIKit-related) on a background thread. Period.

iOS: table view created before xml parsing complete

OK I'm hoping I'm missing something basic here - I am not very expert at this. It should be self-explanatory without example code:
I parse a web-hosted xml file consisting of a list of titles to be displayed in a tableView and associated URLs to pass to a webView when a cell is selected. The parsing happens in the tableView into a dictionary. If I parse on the main thread it works nicely but I'm worried about hanging the UI if the signal is poor. So I wrap the parsing call in a dispatch queue as per examples on here and now it presents an empty table. But if I go back up the view hierarchy and try again (it's embedded in a navigation controller) then it works, there is my table fully populated.
I'm assuming that by using a secondary thread somehow the table is created before the content array is populated. How do I get round this?
Thanks! Andrew
Implement the - (void)parserDidEndDocument:(NSXMLParser *)parser delegate method of NSXMLParser. And call reloadData of your tableView from that method.
- (void)parserDidEndDocument:(NSXMLParser *)parser
{
dispatch_sync(dispatch_get_main_queue(), ^{
[yourTable reloadData];
});
}
Refer NSXMLParserDelegate
If you pares in a dispatch queue you have to update the UI on the main queue.
I am doing something similar. Here is my code:
dispatch_queue_t imgDownloaderQueue = dispatch_queue_create("imageDownloader", NULL);
dispatch_async(imgDownloaderQueue, ^{
NSString *avatarUrlString = [avatarImageDictionary objectForKey:#"url"];
avatarImage = [[UIImage alloc] initWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:avatarUrlString]]];
dispatch_sync(dispatch_get_main_queue(), ^{
id asyncCell = [self.tableView cellForRowAtIndexPath:indexPath];
[[asyncCell avatarImageView] setImage:avatarImage];
});
});

Performing data intensive calculations during view init time

Folks,
I'd like to get your opinions on the following scenario. Most screens on my app are table views where the number of rows and contents of the table view is determined by first reading data from the local core data tables and then performing some complex calculations on it. I'd like to do this in a way where the app does not freeze while the user is transitioning from one screen to another. Here is how I have done it. In the view did appear function I start animating an activity indicator and then spawn a thread to read data from the core data tables and perform all the relevant calculations on it. Inside this thread, upon completion of the calculations, I stop animating the activity indicator, mark a flag that initialization is complete and then reload the table view. Load of table view cells before the initialization is complete will return empty cells. (I noticed that the table view data source functions are called immediately after viewWillAppear and before ViewdidAppear()). Pasted below is my code:
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSLog(#"%s",__FUNCTION__);
}
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(#"%s",__FUNCTION__);
[activityOutlet startAnimating];
dispatch_async(myQueue, ^{ [self getFromCoreData];
});
}
- (void) getFromCoreData {
// Get from coredata and start calculations here
[activityOutlet stopAnimating];
activityOutlet.hidden = YES;
[tableOutlet reloadData];
}
I'd like to know if there is a better way of doing the above.
Thanks in advance for your responses!
UI updates must be done on the main thread:
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self getFromCoreData];
dispatch_async(dispatch_get_main_queue(), ^{
activityOutlet stopAnimating];
activityOutlet.hidden = YES;
[tableOutlet reloadData];
});
});
}
- (void) getFromCoreData {
// Get from coredata and start calculations here
}

Resources