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]; });
Related
I'm writing an iOS app that is getting data from a server. I have several ViewControllers. I used to load data for that viewcontroller under the viewDidLoad method
-(void)ViewDidload
{
[self loadData];
}
-(void)loadData
{
//calling to webservice caller class
}
But this reduces the app's performance. What is the best method to load data within a viewcontroller? For webservice callings, I have a seperate class. Within my loadData method I call to that particular method inside the webservice calling class.
This is going to block my UI.
What do you mean with "this reduces the app performance". Your app is lagging when you are calling your webservice? This is not because you are calling that in viewDidLoad this is because you are doing that in the main thread.
To call your webservice you can use:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
// Call here your web service
dispatch_sync(dispatch_get_main_queue(), ^{
// push here the results to your ViewController
});
});
With this simple solution you are downloading data from your webservice in a separate thread. And pushing the data to your ViewController with the mainThread. This code is not freezing your app. However you will have a moment that nothing happens. This is a good moment to use a UIActivityIndicatorVew.
I guess your interface is lagging.
Try this:
-(void)ViewDidload
{
[NSThread detachNewThreadSelector:#selector(loadData) toTarget:self withObject:nil];
}
I've created an iPad App which internally load a large amount of media, which can freeze the UI for a few seconds (especially on older iPads). I'm exploring async and adding a spinner, but it I haven't been able to identify the right spot for spinner to start prior to new ViewController opening. Any help is appreaciated.
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
[activityIndicatorObject startAnimating];
dispatch_queue_t downloadQueue = dispatch_queue_create("downloader", NULL);
dispatch_async(downloadQueue, ^{
understandingViewController *destViewController = segue.destinationViewController;
destViewController.itemNumber = num1;
destViewController.selectedItem = num1;
dispatch_async(dispatch_get_main_queue(), ^{
[activityIndicatorObject stopAnimating];
});
});
}
Use ViewDidLoad in the destinationViewController to add activityIndicator and startAnimating.
and in the ViewDidLoad , load your data in a background thread
so something like this
- (void)viewDidLoad {
[super viewDidLoad];
// add activity indicator
// start animating
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul);
dispatch_async(queue, ^{
// Perform non main thread operation
// load data
dispatch_sync(dispatch_get_main_queue(), ^{
// perform main thread operation
});
});
}
You sound confused. View controllers are not "in the background". CODE runs in the background. You should not be doing async calls in prepareForSegue.
I would suggest skipping calls to dispatch_async() entirely. Instead, look at using NSURLConnection's sendAsynchronousRequest:queue:completionHandler: method. That will let you submit a connection request (pass in the main queue for the the queue parameter) and a block of code that you want to execute once the connection is complete. The system handles doing the download asynchronously and notifies you when it's done.
I am a newbie. I am using Grand Central Dispatch to populate an array (student_temp) on another thread. That part is working fine. The problem is I cannot pass the array to a class property (student_Array) where it is used throughout the class. I can't get the array back on the main thread.
it works fine until I get back tot he main thread and I can't pass student_temp into student_Array (the property) either inside or outside of GCD.
What am I doing wrong, or is there a better to populate the array property using GCD?
Thank you for your help. And please try to explain in non-technical language if possible I am new at this.
- (void)viewDidLoad
{
[super viewDidLoad];
R2LFetcher *studentFetch = [[R2LFetcher alloc] init];
__block NSMutableArray *student_temp = [[NSMutableArray alloc] init];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
//long-running code goes hereā¦
student_temp = [studentFetch fetchToStudentArray];
dispatch_async(dispatch_get_main_queue(), ^{
// code the updates the main thread (UI) here...
student_Array = student_temp;
});
});
student_Array = student_temp;
A couple of reactions:
In the last line of your code, you're setting student_Array to student_temp. Clearly that line makes no sense because you're populating student_temp asynchronously. And you're opening yourself up to synchronization issues if you're trying to simultaneously access the save variable in two queues. Don't bother to assign student_Array to student_temp at the end of viewDidLoad, but rather just do it inside the nested dispatch_async calls.
Inside the block, you're populating and setting student_temp. It probably makes more sense to make that variable scoped within that block, avoiding temptation to access it from outside that block as well as simplifying your code because the __block qualifier is no longer needed.
This block is running asynchronously, so when you update student_Array in the main queue, you might want to update your UI at the same time (e.g. reload the tableview or whatever). Perhaps you're doing that already and just removed it for the sake of brevity, but I just wanted to make sure.
Thus:
- (void)viewDidLoad
{
[super viewDidLoad];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul);
dispatch_async(queue, ^{
R2LFetcher *studentFetch = [[R2LFetcher alloc] init];
// long-running code goes here, for example ...
NSMutableArray *student_temp = [studentFetch fetchToStudentArray];
dispatch_async(dispatch_get_main_queue(), ^{
student_Array = student_temp;
// code the updates the main thread (UI) here, for example...
[self.tableView reloadData];
});
});
}
You should be able to add objects to student_Array directly from your block. Unlike stack variables, properties and ivars don't get copied when used inside a block. Instead, self gets retained in the block, and the property is referenced through it.
Of course, you need to be aware of concurrency issues, e.g. if you need to access the data from the main thread as well. For that, you probably still want to have this at the end of your async GCD block:
// done populating the data
dispatch_async(dispatch_get_main_queue(), ^{
// update the UI
}
In my iOS application, I have a database call that takes some time to complete. I have a spinner visible on the screen while this operation is taking place. I am hitting an error with the app crashing with "com.myapp failed to resume in time" so it seems like it is running the database call on the main thread, causing issues.
Current Code
-(void)timeToDoWork
{
...
[CATransaction flush];
[[DatabaseWorker staticInstance] doWork];
//Additional UI stuff here
...
if([self->myReceiver respondsToSelector:self->myMessage])
{
[self->myReceiver performSelector:self->myMessage];
}
}
To get the doWork function to take place on a background thread, it looks like I can use Grand Central Dispatch:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
[[DatabaseWorker staticInstance] doWork];
});
However, how do I prevent the execution from continuing until it is complete? Should I end the method after the doWork call, and move everything below it to a new function?
Sample
-(void)timeToDoWork
{
...
[CATransaction flush];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
[[DatabaseWorker staticInstance] doWork];
dispatch_async(dispatch_get_main_queue(), ^{
[self doneDoingWork];
});
});
}
-(void)doneDoingWork
{
//Additional UI stuff here
...
if([self->myReceiver respondsToSelector:self->myMessage])
{
[self->myReceiver performSelector:self->myMessage];
}
}
Is there a better way to do this?
Prevent execution in main thread from continuing is really bad idea. iOS will terminate your application since main thread should always work with run loop.
I suggest you following way to handle your problem:
Write a "Locker". Let it show some view with animated spinner and no buttons at all.
When you start dispatch async operation just bring it to the front and let it work with run loop.
When your async operation completes close the locker.
You can also use blocks.
e.g..
- (void)doWorkWithCompletionHandler:(void(^)())handler {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
// do your db stuff here...
dispatch_async(dispatch_get_main_queue(), ^{
handler();
});
});
}
And then use it like that:
[[DatabaseWorker staticInstance] doWorkWithCompletionHandler:^{
// update your UI here, after the db operation is completed.
}];
P.S.
It might be a good idea to copy the handler block.
The error you are receiving suggests that you are doing something in application:didFinishLaunchingWithOptions: or applicationDidBecomeAction: or somewhere else in the launch cycle that is taking too long and the app is getting terminated by the launch watchdog timer. Above all, it is vital that you return as quickly as possible from these methods. I'm not sure where your code fits into the launch cycle; but this explanation seems plausible.
There are all sorts of ways to address this; but taking the lengthy process off the main queue is the first step as you noted. Without knowing more about what main queue objects (e.g. UI) depend on this database transaction, I'd say that your suggested solution is perfectly fine. That is, dispatch the work to a background queue; and on completion dispatch the remaining UI work to the main queue.
Delegates were suggested elsewhere as a solution. That's also workable although you still have to concern yourself with which queue the delegate methods get called on.
I think that you should use a delegate in your DatabaseWorker and the method doWork always run in background, so when the worker finish the work it tell to its delegate that the work is finished. The delegate method must be called in the main thread.
In the case that you have many objects that need to know when the DatabaseWorker finish instead to use a delegate I would use notifications.
EDIT:
In the DatabaseWorker class you need to implement the method doWork like this:
- (void) doWork{
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
//Do the work.
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate finishWork];
});
});
}
And in the class that implement timeTodoWork:
-(void)timeToDoWork
{
...
[CATransaction flush];
[[DatabaseWorker staticInstance] setDelegate:self];
[[DatabaseWorker staticInstance] doWork];
}
#pragma mark DatabaseWorkerDelegate
- (void) finishWork{
//Additional UI stuff here
...
if([self->myReceiver respondsToSelector:self->myMessage])
{
[self->myReceiver performSelector:self->myMessage];
}
}
Also you can use:
[self performSelectorInBackground:#selector(doWorkInBackground) withObject:nil];
instead of:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
//Do the work.
});
And add a method:
- (void) doWorkInBackground{
//Do the work
[self.delegate performSelectorOnMainThread:#selector(finishWork) withObject:nil waitUntilDone:NO];
}
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.