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.
Related
Ok. So i am trying to use GCD To Handle all the heavy loading before transition to next view controller. I am opening large archive Files and extracting them which takes some time.
The entire Process is like this:
Click a UICollectionViewCell>Display activity indicator>Let GCD take care of heavy loading>call transition selector using performSelector: onThread:.....
The problem is when i use mainThread, the transition occurs too fast and all the heaving loading don't come to effect until after some time and the transition looks awful and while using currentThread, well it just takes so much time, it seems plain awful of a app.
-(void)someMethod
{
//activity Indicator before transition begins
UIActivityIndicatorView *activity=[[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
[activity setFrame:self.view.bounds];
[self.view addSubview:activity];
[self.view bringSubviewToFront:activity];
activity.hidesWhenStopped=YES;
[activity startAnimating];
dispatch_queue_t transitionQueue;
transitionQueue = dispatch_queue_create("com.app.transitionQueue", NULL);
dispatch_async(transitionQueue,^{
//heavy lifting code
viewerPVC=.....
dispatch_async(dispatch_get_main_queue(),^{
[activity stopAnimating];
[self transitionToMangaViewer:mReaderPVC];
});
};
}
-(void)transitionToViewer:(ViewerPVC*)viewerPVC
{
[self.navigationController pushViewController:mReaderPVC animated:YES];
}
So Tried The First Suggestion, but the transition still seems buggy since the CollectionViewController still remain On Background For Some Time after the transition
you shouldnt need to use NSThread when you are using gcd, try something like this instead
dispatch_async(transitionQueue,^{
//heavy lifting code
viewerPVC=..... //this should block here otherwise will not work
dispatch_async(dispatch_get_main_queue(), ^{
[activity stopAnimating];
[self transitionToAnotherViewer:viewerPVC];
});
});
UI Update should be done on Main Thread, no need to create New Thread for performing UI Transition. Try below code:
dispatch_async(transitionQueue,^{
//heavy lifting code
viewerPVC=.....
dispatch_async(dispatch_get_main_queue(), ^{
[activity stopAnimating];
[self performSelector:#selector(transitionToAnotherViewer:) withObject:viewerPVC waitUntilDone:YES];
});
};
While loading when we click on back button or any cell of the table the action is called multiple times after loading is finish.Here the the code snippet that what i'm doing when I start the loading and stop the loading.
+(void)showLoader_OnView{
APP_DELEGATE.window.userInteractionEnabled = NO;
[MBProgressHUD showHUDAddedTo:APP_DELEGATE.window animated:YES];
}
To stop the loading:-
+(void)hideLoader {
APP_DELEGATE.window.userInteractionEnabled =YES;
[MBProgressHUD hideAllHUDsForView:APP_DELEGATE.window animated:YES];
}
please help me.
Update
actually i'm taking data from server. whenever user will go to next window then in viewWillAppear function i call a function which will hit the api to get the data.
-(void)performAutoSync
{
#try
{
if(self.shouldPerformAutoSync)//Necessary conditions to check the auto sync
{
[AppConstants showLoader_OnView]; //here i call the loader.
self.shouldPerformAutoSync = NO;
if(!self.isSyncing)
{
if(!syncBl)
{
syncBl = [[SyncBL alloc] init];
syncBl.delegate = self;
}
if(!syncDl)
syncDl = [[SyncDL alloc] init];
// [self saveModifiedDataForCurrentViewController];
[self delayToAutoSync];
NSMutableDictionary *dictMainData = [NSMutableDictionary new];
[dictMainData setObject:[syncDl fetchCompleteDataAndPrepareDictionary:YES] forKey:#"data"];//#"MainData"];
[syncBl performAutoSync:dictMainData];
}
}
}
#catch (NSException *exception) {
BILog(#"%#",exception);
}
}
Don't block the main thread.
Seeing that you invoke [AppConstants showLoader_OnView] from performAutoSync, and that showLoader_OnView in turn executes:
[MBProgressHUD showHUDAddedTo:APP_DELEGATE.window animated:YES]
I can only assume that performAutoSync is executed in the main thread. This, of course, blocks the UI until your operations are completed.
You should redesign so that you won't need all your state variables, globals, global calls, and take advantage of multi-threading.
Also, remove this, as it qualifies as a kludge;
APP_DELEGATE.window.userInteractionEnabled = NO
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.
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'm using the MBProgressHUD library in my app, but there are times that the progress hud doesn't even show when i query extensive amount of data, or show right after the processing of data is finished (by that time i don't need the hud to be displayed anymore).
In another post i found out that sometimes UI run cycles are so busy that they don't get to refresh completely, so i used a solution that partially solved my problem: Now every request rises the HUD but pretty much half the times the app crashes. Why? That's where I need some help.
I have a table view, in the delegate method didSelectRowAtIndexPath i have this code:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[NSThread detachNewThreadSelector:#selector(showHUD) toTarget:self withObject:nil];
...
}
Then, I have this method:
- (void)showHUD {
#autoreleasepool {
[HUD show:YES];
}
}
At some other point I just call:
[HUD hide:YES];
And well, when it works it works, hud shows, stays and then disappear as expected, and sometimes it just crashes the application. The error: EXC_BAD_ACCESS . Why?
By the way, the HUD object is already allocated in the viewDidLoad:
- (void)viewDidLoad
{
[super viewDidLoad];
...
// Allocating HUD
HUD = [[MBProgressHUD alloc] initWithView:self.navigationController.view];
[self.navigationController.view addSubview:HUD];
HUD.labelText = #"Checking";
HUD.detailsLabelText = #"Products";
HUD.dimBackground = YES;
}
You need to perform your processing on another thread, otherwise the processing is blocking MBProgressHud drawing until it completes, at which point MBProgressHud is hidden again.
NSThread is a bit too low-level for just offloading processing. I'd suggest either Grand Central Dispatch or NSOperationQueue.
http://jeffreysambells.com/2013/03/01/asynchronous-operations-in-ios-with-grand-central-dispatch
http://www.raywenderlich.com/19788/how-to-use-nsoperations-and-nsoperationqueues
/* Prepare the UI before the processing starts (i.e. show MBProgressHud) */
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
/* Processing here */
dispatch_async(dispatch_get_main_queue(), ^{
/* Update the UI here (i.e. hide MBProgressHud, etc..) */
});
});
This snippet will let you do any UI work on the main thread, before dispatching the processing to another thread. It then returns to the main thread once the processing is done, to allow you to update the UI.