I want to make all reading/writing database operations to background queue and update the current UI view when completed.
There is no problem if user stays in the view while I'm dealing with my database. However, if user left that view before database operations completed, it would crash. The psuedo code is as below:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
/* save data to database, needs some time */
dispatch_async(dispatch_get_main_queue(), ^{
// back to main queue, update UI if possible
// here may cause crash
[self.indicator stopAnimating];
[self.imageView ...];
});
});
Try checking if the view is still in the view hierarchy, and also stop the activity indicator from spinning in the viewDidDisappear method as well. You also might need a flag (isNeedingUpdate in the example below) to indicate whether the UI was updated or not, so you can do the appropriate actions if the user goes away before the update is complete and then comes back again.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
if (self.view.window) { // will be nil if the view is not in the window hierarchy
dispatch_async(dispatch_get_main_queue(), ^{
[self.indicator stopAnimating];
[self.imageView ...];
self.isNeedingUpdate = NO;
});
}else{
self.isNeedingUpdate = YES;
});
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if (isNeedingUpdate) {
// do whatever you need here to update the view if the use had gone away before the update was complete.
}
}
-(void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
[self.indicator stopAnimating];
}
Related
When I do UI updates from the main thread, they do not seem to take effect straight away. That is, the changes do not appear immediately on screen.
Here is a simplified version of the code I'm running:
- (void) do_ui_update {
// UI update here that does not appear immediately
}
- (void) some_time_consuming_function {
// line 1
// line 2
// ...
// line n
}
- (void) function_that_runs_in_main_thread {
[self RUN_ON_UI_THREAD:^{
[self do_ui_update];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self some_time_consuming_function];
});
}];
}
- (void) RUN_ON_UI_THREAD:(dispatch_block_t)block
{
if ([NSThread isMainThread])
block();
else
dispatch_sync(dispatch_get_main_queue(), block);
}
When I debug setting breakpoints at each line in some_time_consuming_function, sometimes the UI updates appear on screen when the debugger hits line 2, sometime line 3, and so on.
So my question is:
How can I make the UI updates appear on screen before the very first line of some_time_consuming_function is reached?
Dispatch the background dispatch to the next main loop iteration:
-(void) function_that_runs_in_main_thread {
[self do_ui_update];
dispatch_async(dispatch_get_main_queue(), ^{
// All pending UI updates are now completed
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self some_time_consuming_function];
});
});
}
I'd like to stop the animation of the indicator within a method called by default NSNotificationCenter with a postNotificationName.
So I'm doing this on Main Thread
-(void)method
{
...
[ind performSelectorOnMainThread:#selector(stopAnimating) withObject:nil waitUntilDone:NO];
}
It does'n work. Method is called correctly, any other called selectors do their job but not stopAnimating.
I put [ind stopAnimating] in another function and then called it via performSelectorOnMainThread but it still didn't worked.
Try this...
Create a method that stops your animation
-(void)stopAnimationForActivityIndicator
{
[ind stopAnimating];
}
Replace your method like this -
-(void)method
{
...
[self performSelectorOnMainThread:#selector(stopAnimationForActivityIndicator) withObject:nil waitUntilDone:NO];
}
Should do the magic...
You can also use the below method which starts and stops the activity indicator on main thread in a single method, also provides you to execute your code asynchronously as well-
- (void)showIndicatorAndStartWork
{
// start the activity indicator (you are now on the main queue)
[activityIndicator startAnimating];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// do your background code here
dispatch_sync(dispatch_get_main_queue(), ^{
// stop the activity indicator (you are now on the main queue again)
[activityIndicator stopAnimating];
});
});
}
Try :
-(void)method
{
dispatch_async(dispatch_get_main_queue(), ^{
[ind stopAnimating];
});
}
I am running an update on an Sqlite3 database in the background when the user presses a force update button.
I want to disable the button as to not lock the database and keep the user from pressing it over and over again. Plus I want to show an Activity Indicator. However, the button is not disabling and the activity indicator does not show.
What am I doing wrong?
I hide the activity indicator when the view is loaded.
Built with storyboards:
View did load
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
//other going on
[self updateUIInterface:false];
}
The method to update the UI
- (void) updateUIInterface : (BOOL) updating {
if (updating) {
//Disable buttons and show activity indicator
self.actLocalDB.hidden = NO;
[self.actLocalDB startAnimating];
self.btnSyncLocal.enabled = NO;
[self.btnSyncLocal setTitle:#"Updating.." forState:UIControlStateDisabled];
[self.btnSyncLocal setUserInteractionEnabled:NO];
} else {
// Enable buttons
self.actLocalDB.hidden = YES;
[self.actLocalDB stopAnimating];
self.btnSyncLocal.enabled = YES;
[self.btnSyncLocal setTitle:#"Sync Databases" forState:UIControlStateDisabled];
[self.btnSyncLocal setUserInteractionEnabled:YES];
}
}
My method to update the DB
- (IBAction)syncLocalDB:(id)sender {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(#"Begin Local DB Sync");
[self updateUIInterface:true];
//db stuff goes here
dispatch_async(dispatch_get_main_queue(), ^{
//update UI here
NSLog(#"Done updating local db");
[self updateUIInterface:false];
});
});
}
You can't make UI changes in background threads. All UI operations need to be performed on the main thread. Here is a nice blog post on the topic and a link to the docs.
Just call updateUIInterface Method before entering the GCD-Block.
- (IBAction)syncLocalDB:(id)sender {
[self updateUIInterface:true];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(#"Begin Local DB Sync");
//db stuff goes here
dispatch_async(dispatch_get_main_queue(), ^{
//update UI here
NSLog(#"Done updating local db");
[self updateUIInterface:false];
});
});
}
I am developing an app where the user will get to confirm some action via UIAlertView, if he confirms, I call a method that handles the operation, then I prepare to pop the view I am in to go back to another view after the method has been called.
I want to show UIActivityIndicatorView if the user presses confirm for as long as it takes to execute the method and go to that other view. I used startAnimating and stopAnimating in the proper location, but i never get to see the UI UIActivityIndicatorView shown, not for a sec.
I guess its related to some UI issues due to UIAlertView, not sure if I am correct though. I just need a clue on how to use UIActivityIndicatorView properly for a method execution time.
My code:
- (void)viewDidLoad
{
[super viewDidLoad];
self.activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
self.activityIndicator.alpha = 1.0;
self.activityIndicator.hidesWhenStopped = YES;
self.activityIndicator.center = self.view.center;
[self.view addSubview:self.activityIndicator];
}
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
if(buttonIndex == 1) {
[self.activityIndicator startAnimating];
ContactsTableViewController *contactTableView = [self getContactsTVC];
[contactTableView applyActionOnCells];
// doing some setup before poping off to the root view controller of my nav controller
[self.activityIndicator stopAnimating];
// then go to rootViewController
[self.navigationController popToRootViewControllerAnimated:YES];
}
}
I'm not 100% certain, but try to comment out the stopAnimating call and see if it shows up.
If that helps, applyActionOnCells probably blocks your main thread (where all UI stuff also happens) and the indicator never has a chance to show up before you hide it again.
In that case, try do the applyActionOnCells call in the background:
if(buttonIndex == 1) {
[self.activityIndicator startAnimating];
__block typeof(self) bself = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
ContactsTableViewController *contactTableView = [bself getContactsTVC];
[contactTableView applyActionOnCells];
dispatch_async(dispatch_get_main_queue(), ^{
[bself.activityIndicator stopAnimating];
// then go to rootViewController
[bself.navigationController popToRootViewControllerAnimated:YES];
});
});
}
Edit: see also an earlier question.
What i want to achieve :
Create processing screen with some custom icons, label etc. with following behaviour.
Add view over window which will not allow user to touch anything in application until it is removed. (like processing/loading screen)
When this view is displayed all other operation like adding subview, performing segue etc should work as they work normally but below my loading view.
Want method showProcessingScreen to work on any thread (Whatever thread switching code etc should be in respective show/hide method).
It should be displayed/removed immediately in after calling respective methods.
Code :
-(void) showProcessingScreen
{
dispatch_async(dispatch_get_main_queue(),
^{
UIStoryboard *mystoryboard = [UIStoryboard storyboardWithName:#"Main_iPhone" bundle:nil];
processingScreen = [mystoryboard instantiateViewControllerWithIdentifier:#"loadingViewController"];
UIWindow* mainWindow = [[UIApplication sharedApplication] keyWindow];
[mainWindow addSubview: processingScreen.view];
[mainWindow bringSubviewToFront:processingScreen.view];
});
}
-(void) hideProcessingScreen
{
dispatch_async(dispatch_get_main_queue(),
^{
[processingScreen.view removeFromSuperview];
});
}
Issue:
I want code above to work with showing/hiding loading screen immediately.
- (IBAction)proceedBtnPressed:(id)sender
{
[[GUIUtilities sharedObj] showProcessingScreen];
//Some other code here
}
When i call showProcessingScreen like above processing screen takes around 2-3 sec to show.
But when i remove other code below it (//Some other code) it shows screen immediately.
What i have tried:
Putting code in showProcessingScreen in other method and calling that on main thread using performSelectorOnMainThread.
Calling showProcessingScreen on background and executing show code on main thread using performSelector.
This works
//code
-(IBAction)proceedBtnPressed:(id)sender
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0),
^{
[[GUIUtilities sharedObj] showProcessingScreen];
//Some other code here
});
}
But i don't want any thread switching mechanism outside of showProcessingScreen.
This is common screen almost used in every application. I used similar codes with xib, custom views in my previous apps which were not using storyboard etc.,
I know this is related to threading, what i am doing wrong here ? what is best practice to achieve this ?
Any help will be aprreciatead.
The problem is that when you're dispatching, you put the creation of your loading view at the end of the run loop. Whatever your "other code" is is blocking the main thread for a few seconds.
If you move this "other code" to a different thread, this will resolve your issue. (This solution is ideal.)
You could also only switch to the main thread conditionally, which would resolve the issue if you call the loading view from the main thread:
-(void) showProcessingScreen
{
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:#selector(showProcessingScreen) withObject:nil waitUntilDone:FALSE];
return;
}
UIStoryboard *mystoryboard = [UIStoryboard storyboardWithName:#"Main_iPhone" bundle:nil];
processingScreen = [mystoryboard instantiateViewControllerWithIdentifier:#"loadingViewController"];
UIWindow* mainWindow = [[UIApplication sharedApplication] keyWindow];
[mainWindow addSubview: processingScreen.view];
[mainWindow bringSubviewToFront:processingScreen.view];
}
Instead of adding an overlay view that intercepts touch events, you can just call
– beginIgnoringInteractionEvents
– endIgnoringInteractionEvents
You could try with this:
- (void)doSomeOtherCode {
//Some other code here
}
- (IBAction) showProcessingScreenAndDoSomeOtherCode:(id)sender
{
[[GUIUtilities sharedObj] showProcessingScreen];
[self performSelector:#selector(doSomeOtherCode) withObject:nil afterDelay:0.0];
}
- (IBAction)proceedBtnPressed:(id)sender
{
[self showProcessingScreenAndDoSomeOtherCode:self];
}
and it will work. About your remark:
But i don't want any thread switching mechanism outside of showProcessingScreen.
The above solution will not cause any thread switching outside of showProcessingScreen. performSelector will just add an entry in the event loop queue.
EDIT:
If you are using blocks, the same result can be achieved through:
dispatch_async(dispatch_get_main_queue(),
^{
[[GUIUtilities sharedObj] showProcessingScreen];
dispatch_async(dispatch_get_main_queue(), ^{
//Some other code here
});
});
or:
dispatch_async(dispatch_get_main_queue(),
^{
[[GUIUtilities sharedObj] showProcessingScreen];
});
dispatch_async(dispatch_get_main_queue(), ^{
//Some other code here
});
Since the main queue is a serial queue, showProcessingScreen and "Some other code here" would be executed serially.