In my model, data is downloaded from website in a for loop and in a each turn data is sending to my viewController using protocol method. In model file;
for(NSString* data in DataArray){
[self.delegate passUpdatingCourse:data_name];
//other operations
}
In my viewController data name coming from model is saving to NSArray property in other thread;
ModelClass *modelObject = [[ModelClass alloc] init];
[modelObject setDelegate:self];
dispatch_queue_t otherQ = dispatch_queue_create("Q", NULL);
dispatch_async(otherQ, ^{
//other operations
[self performSelectorOnMainThread:#selector(passUpdatingCourse:) withObject:nil waitUntilDone:YES];
dispatch_async(dispatch_get_main_queue(), ^{
[self.myIndicator stopAnimating];
self.indicatorText.hidden = YES;
[self.changingCourseLabel setNeedsDisplay];
});
});
And also data coming via protocol method is setting viewController label;
-(void)passUpdatingCourse:(NSString *)data_in{
self.myLabel.text = data_in;
}
When each data came, myLabel in a viewController must be update. But it is not happens. In a protocol method when I use this;
NSLog(#"Data:%#",self.myLabel.text);
Yeah it shows data in a console but myLabel in a view is not changing.
I searched questions like that but couldn't find a solution.
Assuming your loop is on a background thread, dispatch your label updates to the main queue (async).
If you're inside a for loop on the main thread nothing is going to get updated in the UI until your method returns and dispatch won't help in that case.
Related
What is the correct way to fire methods within a completion block (if this is even recommended)? Right now, I have an IBAction that calls a method that downloads information with a completion block signifying if the info was retrieved successfully or not. If it was, I want to push a view controller that will display that information, but at the moment, nothing is happening. I'm guessing it has something to do with main thread, gcd, etc...
__weak YTTMSetupViewController *weakSelf = self;
[mc downloadJson:^(BOOL success) {
if(success){
NSLog(#"sucess. metric count - %i",(int)mc.collection.count);
//info was downloaded. Push new view controller with info
YTTMMetricTableViewController *mtvc = [self.storyboard instantiateViewControllerWithIdentifier:#"YTTMMetricTableViewController"];
mtvc.group = (WAGroup*)[[WAMetricCollection sharedInstance].collection lastObject];
mtvc.hidesBottomBarWhenPushed = YES;
[weakSelf.navigationController pushViewController:mtvc animated:YES];
}
else{
NSLog(#"failure");
//display failure UI
}
NSLog(#"end of downloading");
[HUD dismissAfterDelay:0.5f animated:YES];
}];
Not sure if this is the right way to do it, but it worked.
I added a method that will push the vc on the main thread as so:
[weakSelf performSelectorOnMainThread:#selector(pushDetail) withObject:nil waitUntilDone:YES];
Completed Code:
__weak YTTMSetupViewController *weakSelf = self;
[mc downloadJson:^(BOOL success) {
if(success){
NSLog(#"sucess. metric count - %i",(int)mc.collection.count);
//info was downloaded. Push new view controller with info
[weakSelf performSelectorOnMainThread:#selector(pushDetail) withObject:nil waitUntilDone:YES];
}
else{
NSLog(#"failure");
//display failure UI
}
NSLog(#"end of downloading");
}];
}
-(void)pushDetail{
__weak YTTMSetupViewController *weakSelf = self;
YTTMMetricTableViewController *mtvc = [self.storyboard instantiateViewControllerWithIdentifier:#"YTTMMetricTableViewController"];
mtvc.group = (WAGroup*)[[WAMetricCollection sharedInstance].collection lastObject];
mtvc.hidesBottomBarWhenPushed = YES;
[weakSelf.navigationController pushViewController:mtvc animated:YES];
}
You could simply try wrapping the call with a dispatch_asynch block...
__weak YTTMSetupViewController *weakSelf = self;
[mc downloadJson:^(BOOL success) {
if(success){
NSLog(#"sucess. metric count - %i",(int)mc.collection.count);
dispatch_async(dispatch_get_main_queue(), ^{
//info was downloaded. Push new view controller with info
YTTMMetricTableViewController *mtvc = [self.storyboard instantiateViewControllerWithIdentifier:#"YTTMMetricTableViewController"];
mtvc.group = (WAGroup*)[[WAMetricCollection sharedInstance].collection lastObject];
mtvc.hidesBottomBarWhenPushed = YES;
[weakSelf.navigationController pushViewController:mtvc animated:YES];
});
}
else{
NSLog(#"failure");
//display failure UI
}
NSLog(#"end of downloading");
[HUD dismissAfterDelay:0.5f animated:YES];
}];
All UI updates must be performed on the main thread. Personally I prefer to do this through GCD as it produces more readable code than performSelectorOnMainThread. However, there's nothing wrong with performSelectorOnMainThread aside from personal preference in the case of calling a single UI update on the main thread following the execution of some completion block. Do note that, whichever one you choose, you should be consistent with what you use to guarantee that blocks are enqueued in the order you specified.
Working code aside, however, the convention Apple's frameworks seem to use is to perform all completion blocks on the main thread unless a queue is specified as a method parameter, in which case the completion block should be performed on that queue. So in this case I would recommend you edit your download handler class's downloadJson method to automatically perform the completion block on the main queue.
I am having some trouble updating my UI using performSelectorOnMainThread. Here is my situation. In my viewDidLoad I set up an activity indicator and a label. Then I call a selector to retrieve some data from a server. Then I call a selector to update the UI after a delay. Here's the code:
- (void)viewDidLoad
{
[super viewDidLoad];
self.reloadSchools = [[UIAlertView alloc] init];
self.reloadSchools.message = #"There was an error loading the schools. Please try again.";
self.reloadSchools.title = #"We're Sorry";
self.schoolPickerLabel = [[UILabel alloc]init];
self.schoolPicker = [[UIPickerView alloc] init];
self.schoolPicker.delegate = self;
self.schoolPicker.dataSource = self;
self.server = [[Server alloc]init];
schoolList = NO;
_activityIndicator = [[UIActivityIndicatorView alloc]initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
[self.view addSubview:_activityIndicator];
[self.view bringSubviewToFront:_activityIndicator];
[_activityIndicator startAnimating];
[NSThread detachNewThreadSelector: #selector(getSchoolList) toTarget: self withObject: nil];
[self performSelector:#selector(updateUI) withObject:nil afterDelay:20.0];
}
The selector updateUI checks to see if the data was retrieved, and calls a selector on the main thread to update the UI accordingly. Here is the code for these parts:
-(void)updateUI
{
self.schools = [_server returnData];
if(!(self.schools == nil)) {
[self performSelectorOnMainThread:#selector(fillPickerView) withObject:nil waitUntilDone:YES];
}
else {
[self performSelectorOnMainThread:#selector(showError) withObject:nil waitUntilDone:YES];
}
}
-(void)showError {
NSLog(#"show error");
[_activityIndicator stopAnimating];
[self.reloadSchools show];
}
-(void)fillPickerView {
NSLog(#"fill picker view");
schoolList = YES;
NSString *schoolString = [[NSString alloc] initWithData:self.schools encoding:NSUTF8StringEncoding];
self.schoolPickerLabel.text = #"Please select your school:";
self.shoolArray = [[schoolString componentsSeparatedByString:#"#"] mutableCopy];
[self.schoolPicker reloadAllComponents];
[_activityIndicator stopAnimating];
}
When the selector fillPickerView is called the activity indicator keeps spinning, the label text doesn't change, and the picker view doesn't reload its content. Can someone explain to me why the method I am using isn't working to update my ui on the main thread?
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//load your data here.
dispatch_async(dispatch_get_main_queue(), ^{
//update UI in main thread.
});
});
First of all you should not be using detachNewThreadSelector. You should use GCD and submit your background task to an async queue. Threads are costly to create. GCD does a much better job of managing system resources.
Ignoring that, your code doesn't make a lot of sense to me. You submit a method, getSchoolList, to run on a background thread. You don't show the code that you are running in the background.
Then use performSelector:withObject:afterDelay to run the method updateUI on the main thread after a fixed delay of 20 seconds.
updateUI checks for self.schools, which presumably was set up by your background thread, and may or may not be done. If self.schools IS nil, you call fillPickerView using performSelectorOnMainThread. That doesn't make sense because if self.schools is nil, there is no data to fill the picker.
If self.schools is not nil, you display an error, again using performSelectorOnMainThread.
It seems to me that the logic on your check of self.schools is backwards. If it is nil you should display an error and if it is NOT nil you should fill the picker.
Next problem: In both cases you're calling performSelectorOnMainThread:withObject:waitUntilDone: from the main thread. Calling that method from the main thread doesn't make sense.
Third problem: It doesn't make sense to wait an arbitrary amount of time for a background task to run to completion, and then either succeed or fail. You won't have any idea what's going on for the full 20 seconds. If the background task finishes sooner, you'll never know.
Instead, you should have your background task notify the main thread once the task is done. That would be a valid use of performSelectorOnMainThread:withObject:waitUntilDone:, while calling it from the main thread is not. (Again, though, you should refactor this code to use GCD, not using threads directly.
It seems pretty clear that you are in over your head. The code you posted needs to be rewritten completely.
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");
}];
}
Hopefully it is an appropriate question to ask. My goal is
1.add a controller into an array name 'arrControllers'
2.access and get current controller from 'arrControllers' to do sth with it
and I just wanna make sure the arrControllers should be ready before I access and use it. That is why I am using serial queue and dispatch_sycn like like following.
-(void)viewDidLoad
{
[super viewDidLoad];
firstViewController = [[FirstViewController alloc] initWithNibName:#"FirstViewController" bundle:nil];
[self setUpWithView:firstViewController];
}
and in setUpWithView: is
-(void)setUpWithView:(UIViewController*)viewController {
dispatch_queue_t queue;
queue = dispatch_queue_create("my queue", NULL);
containerController = [ContainerViewController sharedContainerController];
// What I am taking about is from now on
dispatch_sync(queue, ^{
[containerController initWithRootViewController:viewController];
});
dispatch_sync(queue, ^{
[containerController setUpView];
});
}
and initWithRootViewController: is
- (void)initWithRootViewController:(UIViewController*)rootViewController {
arrControllers = [[NSMutableArray alloc] init];
arrControllers = [NSMutableArray arrayWithObject:rootViewController];
}
and setUpView is
-(void)setUpView {
/* Process to ADD currentController TO hierarchy */
[self addChildViewController:[arrControllers lastObject]];
............................................................
}
As far as I know the compiler will execute codes line by line, it means by do following
-(void)setUpWithView:(UIViewController*)viewController {
containerController = [ContainerViewController sharedContainerController];
// Not using serial_queue and dispatch_sync
[containerController initWithRootViewController:viewController];// line1
[containerController setUpView]; //line2
}
compiler will execute line2 until it finished the task at line1.
My question is
1. is it necessary to using `serial_queue` and `dispatch_sycn`for this situation.
PS: if you think it is a bad question to ask. please comment what you think about it.
First of all, you should have a look at UIView and UIViewController life cycles, this will tell you when a UIVie or a UIViewController is "ready to be used". Second, you should check when the serial ques and GCD is used.
For the second point I can give you a short summary because it's kind of faster that the first point.
Use CGD and queues (other then main queue) if you want to do computing or other stuff, that don't involve UI changes, on a background thread without freezing the UI.
Use GCD with main queue to switch between a background queue and the main (UI) queue.
All UI operations must be performed on the main thread.
So in your case, you want to create a view controller and store it into an array, so as a suggestion, all the UI related calls of your view controllers should be performed on the UI thread, so no need for GCD or background threads.
In case you are doing some complicated stuffs on your view controller's init methods, just put those complicated stuffs on a GCD block on a separated thread.
In this situation if you do not use dispatch_sync, you will get exactly what you need:
-(void)setUpWithView:(UIViewController*)viewController {
containerController = [ContainerViewController sharedContainerController];
// Not using serial_queue and dispatch_sync
[containerController initWithRootViewController:viewController];// line1
[containerController setUpView]; //line2
}
Line 1 will execute initWithRootViewController:, going deeper and executing lines
arrControllers = [[NSMutableArray alloc] init];
arrControllers = [NSMutableArray arrayWithObject:rootViewController];
then it will return from this function one level above and proceed, moving to line 2 and going down to line
[self addChildViewController:[arrControllers lastObject]];
ALSO. Never write code like this
arrControllers = [[NSMutableArray alloc] init];
arrControllers = [NSMutableArray arrayWithObject:rootViewController];
First line creates an empty NSMutableArray and assigns a pointer to it into arrControllers.
Second line creates another array (autoreleased) and assigns a pointer to it in arrControllers again. So a link to array created in the first line is lost and it is leaked under manual memory management.
If you use manual memory management, do the following:
arrControllers = [[NSMutableArray alloc] init];
[arrControllers addObject:rootViewController];
OR
arrControllers = [[NSMutableArray alloc] initWithObjects:rootViewController,nil];
Under ARC leave only second line.
Hope I understood you correctly. Feel free to ask questions.
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]; });