Reputation: 35
XCode 4.5, iPad development, iOS6
Hi, I hope you can help a novice developer! Apologies in advance if this has already been answered but I could not find during my searches!
I am developing an app that needs to import a large amount of data into Core Data. The import routine works fine (alert shows 'Please wait' with activity monitor while routine works in the background) but I want to give the users more detailed feedback on the progress of the import (such as 'XX% imported'). The following code kicks the process off and -
- (IBAction)import:(id)sender{
[self showWaiting];
[self performSelectorInBackground:(@selector(callGrouper)) withObject:nil];
}
-(void)showWaiting{
alertMsg = @"Please Wait....";
waitAlert = [[UIAlertView alloc] initWithTitle:alertMsg message:nil delegate:self cancelButtonTitle:nil otherButtonTitles: nil];
[waitAlert show];
UIActivityIndicatorView *indicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
indicator.center = CGPointMake(waitAlert.bounds.size.width / 2, waitAlert.bounds.size.height - 50);
[indicator startAnimating];
[waitAlert addSubview:indicator];
}
-(void)callGrouper{
ImportRoutine *firstTest = [[ImportRoutine alloc] init];
[firstTest runImport:managedObjectContext];
[waitAlert dismissWithClickedButtonIndex:0 animated:TRUE];
UIAlertView *alert = [[UIAlertView alloc]initWithTitle: @"iPad Application"
message: @"Import complete!"
delegate: self
cancelButtonTitle:@"Ok"
otherButtonTitles:nil];
[alert show];
}
Within ImportRoutine (separate class) I have code that gathers data on percentage imported but how can I pass this message back to the main thread so I can update 'alertMsg' and in turn update the UIAlertView?
Upvotes: 3
Views: 1750
Reputation: 10782
You can dispatch blocks of code back onto the main thread using GCD (grand central dispatch):
dispatch_async(dispatch_get_main_queue(), ^{
// code here to update UI
});
Any object in the scope of the method that contains the dispatch call gets retained which makes it easy to pass objects back into the main thread without worrying about the background thread being deallocated along with its objects before you've had a chance to process the data. Primitive values in the local scope (aka int, float, double, etc) are copied, so if you set an int to 5, dispatch a block where you print the value of the int, and then right after set the int to 10, even if the block executes after you set the int to 10 it'll still print 5. Note that You can't mutate the same mutable object (such as `NSMutableArray or NSMutableDictionary) in two threads at the same time or mutate in one and enumerate in another without crashing so you'll want to be careful about doing something like that (thanks goes to @andrewmadsen for reminding me to warn you).
dispatch_async()
, unlike dispatch_sync()
, will not wait for the code that's dispatched to complete before continuing execution which is nice since your background thread doesn't need to care if things in the UI have finished.
You could stick the dispatch call inside of the method on the ImportRoutine
class that calculates the progress as long as your UIAlertView is addressable outside of your view controller class. Or if you want to follow model-view-controller design principals more closely, you could create a method like so in your view controller:
- (void)updateProgressToPercentComplete:(double)percent {
if ([NSThread currentThread] != [NSThread mainThread]) {
dispatch_async(dispatch_get_main_queue(), ^{
// update code or call to method that is guaranteed to be on the main thread.
}
}
else {
// update code or call to method that is guaranteed to be on the main thread.
}
}
If you've gone into the documentation and now you're all like "oh my gosh Objective-C blocks are the coolest thing ever" you could modify the method above so you don't need to write the same update code twice:
- (void)updateProgressToPercentComplete:(double)percent {
void (^updateProgressBlock)(void) = ^{
// update code
};
if ([NSThread currentThread] != [NSThread mainThread]) {
dispatch_async(dispatch_get_main_queue(), updateProgressBlock());
}
else {
updateProgressBlock();
}
}
By the way I noticed in your -callGrouper
code that you're using an existing managedObjectContext that I assume you created on the main thread in a background thread... most of core data isn't threadsafe so you need to be extremely careful or you will crash all over the place. You might be better off creating a secondary managed object context on the background thread and then merging changes into the context on the main thread (or save on the background thread and re-fetch on the main thread).
Edit:
Basic flow: Begin your background process from your view controller and pass in a progress block. -> Import class in the background thread executes your progress block periodically -> Inside your progress block you dispatch back to the main thread to update UI.
In your ImportRoutine class add a property declaration like so:
@property (nonatomic, strong) void (^progressBlock)(NSUInteger);
Which means a property called progressBlock
that takes an unsigned integer (0-100) and doesn't return anything (void). You should make this property private by using a class extension.
Then you'll want to create a method in your import class like so:
- (void)callGrouper:(void (^)(NSUInteger))progress {
[self setProgressBlock:progress];
// Your import code
}
In your method where you receive progress updates, call the progressBlock and pass in your progress as a number between 0 and 100:
if ([self progressBlock] != nil) {
[self progressBlock](progressValue);
}
Notice that I check to make sure the progress block isn't nil. You would crash and burn if you tried to execute a NULL block.
Then you can pass in a block as the object in your import routine call you already have in the view controller and inside the block dispatch back to the main queue and update your progress.
Upvotes: 2
Reputation: 577
You can use:
[self performSelectorOnMainThread:@selector(yourSelector) withObject:anObjectIfYouNeedToSendOne waitUntilDone:YES/NO];
The UI runs on main thread and so you cand acces again your UIAlertView or other UI object.
Upvotes: 1